frostutil

package module
v1.2.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 15, 2023 License: BSD-3-Clause Imports: 15 Imported by: 1

README

This package contains a number of utility functions related to math, strings, functional things (map, compose, partial application), colors, and images (*ebiten.Image, *image.NRGBA, and *image.RGBA), as well as a framework to enable running tests under Ebitengine:

In util.go:

  • Min and Max functions for all signed or unsigned integers and floats, using generics.
  • Abs function for signed integers or floats, using generics.
  • Split function that splits a string by a separator rune, but not when that rune is prefixed by a backslash. It also unescapes escaped separators and endlines.
  • Join function that does the opposite of Split: It takes a slice of strings, and joins them with a specified separator, while escaping all already-existing instances of that separator and of endlines.
  • UnescapeStr and EscapeStr, which just do the separator and endline escaping/unescaping without the splitting/joining.
  • SearchStringsUnsorted, which searches an unsorted slice of strings to see if any of them are identical to a particular string, returning the index if it is found. This doesn't sort the slice and I don't recommend using it often, because it is nowhere near as efficient as using a proper search algorithm to search a sorted slice of strings.
  • Xor for two boolean values, because Go lacks a boolean xor operator.
  • DegreesToRadians and RadiansToDegrees convenience functions.
  • A CenterString function, which takes a string and a desired length, and returns the string padded on both sides with spaces, if the desiredLen is long enough. If len(str) <= desiredLen, the string is returned unchanged. Otherwise, it adds as many spaces as necessary to make the string's length match desiredLen, using an equal number of spaces on both sides unless an odd number of spaces need to be added, in which case the final space is placed on the right side.
  • A Compose function which composes two functions which each take a single parameter and return value of the same type. That is, it takes (f (T) T) and (g (T) T) and returns a function which takes a parameter x of type T, which when called will call f(g(x)) and return whatever it returns.
  • A Map function which takes a function parameter f and a slice parameter xs, and returns a new slice ys created from the results of calling the function f on each element x of the slice xs.
  • A Partial2to1 function which does partial application of a two-parameter function, which could be useful for use with Compose or Map. I would like to have written a function that could do partial application of any arbitary function, but I'm pretty sure that's impossible. It would require being able to specify a function with an arbitrary number of parameters (which doesn't use ...) and return values. With this example, though, it should be easy to write a function to do partial application for functions with any arbitary number of parameters etc.

In color.go, a number of functions for 32bpp colors (8 bit color and alpha components):

  • ToNRGBA, which converts any arbitary standard color.Color type to NRGBA and returns the individual red, green, blue, and alpha bytes, without using the model convert functions. Unlike the color package's conversion stuff, when alpha is zero, this still preserves the color components, rather than returning them as zero also. Since normally the color components are zeroed when using the model conversion functions or NRGBA color's RGBA() method when alpha is zero, this capability is only relevant when you're manually modifying an image's pixel buffer. If you want to preserve color information when encoding from NRGBA to RGBA, you can use MultiplyAlphaBytesPreserveColors.
  • ToNRGBA_Color, which is the same but returns a color.Color.
  • ToNRGBA_U32, which is the same but packages the output into a uint32 where the bytes are from high to low: red, green, blue, and alpha.
  • UnmultiplyAlpha, which is what ToNRGBA calls when the color it is given is RGBA or RGBA16 or an unrecognized Color type. It can be called directly to skip the switch statement in ToNRGBA. This also preserves color components when alpha is zero. It calls the color's RGBA() method to get the color bytes in RGBA format, then unmultiplies the alpha, if it is neither zero nor 0xffff (RGBA() returns 16-bit color and alpha components).
  • UnmultiplyAlphaBytes, which takes the four components as alpha-premultiplied bytes, rather than a color.Color, and returns four components as bytes with the alpha premultiplication removed. That is to say, it converts RGBA bytes to NRGBA bytes. This also preserves color components when alpha is zero. Unlike UnmultiplyAlpha, it doesn't work with arbitary color types.
  • MultiplyAlphaBytes, which does the opposite of UnmultiplyAlphaBytes: It converts NRGBA bytes to RGBA bytes. This is more of a convenience function so you don't have to write code to pack the bytes into a color, call RGBA(), and then unpack them.
  • MultiplyAlphaBytesPreserveColors, which is like MultiplyAlphaBytes but it preserves the color components when the alpha component is zero. It does the math itself rather than calling RGBA(). It gives results that match what you get from MultiplyAlphaBytes() except for when alpha is 0.

In image.go:

  • NewImageFromEImage, which converts an *ebiten.Image to an *image.RGBA by retrieving the raw RGBA pixel data and copying it to a new image, which it returns. We do this so that we can save *ebiten.Images as PNGs, since attempting to directly feed an *ebiten.Image to png.Encode results in garbage output. This is useful for screenshots, for example.
  • NewEImageFromImage, which creates a new *ebiten.Image from an arbitary image type. It does so by creating a new *ebiten.Image of the same size, and copying the pixel data from the source image. In theory, that's what ebiten.NewImageFromImage should also be doing, but in practice I found that it was for some reason corrupting the source images fed to it (in Ebitengine 2.3.*, anyways). This has a bool mipmaps parameter, so that you can easily say whether the new image should have mipmaps or not. Also, this is designed to be able to quickly and efficiently copy *ebiten.Images and *image.RGBA images. It copies *image.NRGBA images more slowly, since it has to convert their pixel data to RGBA before it can copy it to the new *ebiten.Image. Any other image type is copied very slowly, since it won't have access to the pixel data buffer, and will have to copy pixels one by one using At and Set.
  • CopyImage, which quickly and efficiently copies an image's pixel data to a new image of the same type (*ebiten.Image, *image.NRGBA, or *image.RGBA) and returns the copy. If given any other type of image, it creates a new *image.RGBA and copies the pixel data into it very slowly using At and Set.
  • CopyImageLines copies image data line by line. It is slower than copying the entire pixel data buffer at once, but useful if the source and destination images have different strides (because of padding, for instance). As far as I know, this shouldn't come up with images loaded from PNGs, but it might with other image formats.
  • SlowImageCopy copies pixel data from iImg to oImg pixel by pixel using (Image).At and (Image).Set. It's called by CopyImage or NewEImageFromImage if iImg isn't an *ebiten.Image, *image.NRGBA, or *image.RGBA. Since image.Image doesn't have a Set method, oImg must still be one of those three for this to work. If it isn't one of those, it returns an error.

In matchesImage.go:

  • MatchesImage, which takes a *testing.T, an image name, and an image, and compares the image to the expected output (which should be a .png file in testdata/expected). If it fails to match, or the expected image is missing, it reports a failure to the *testing.T, attempts to write the failed image to testdata/failed (creating the folder if it doesn't exist), and returns false. If it matches, it returns true. It accepts both regular images and *ebiten.Images. If you just didn't have an expected image yet and it is correct, you can move the output image from testdata/failed to testdata/expected and the next run should pass, assuming the output is the same every time.

Finally, test.go contains the code that enables testing things under Ebitengine in the Layout, Update, and Draw methods. To use this, every package that needs to test things under Ebitengine first needs a single file whose name should start with "test" which contains this function:

func TestMain(m *testing.M) {
	frostutil.OnTestMain(m)
}

I personally keep this in main_test.go. You can see one in this package, since image_test.go includes tests that run under Ebitengine.

OnTestMain ensures that every test run in the package under Ebitengine runs in the main/OS thread. It sets up and runs Ebitengine, runs your test functions (via m.Run) (which should call QueueLayoutTest, QueueUpdateTest, and/or QueueDrawTest if they need to run test code under Layout, Update, or Draw), waits for m.Run and all the tests to finish, and then prompts Update to tell Ebitengine to shut down.

And then for the test files you have where you want to test things under Ebitengine, you can write tests like so (note that you don't have to queue them all from one test function, this is just to show all three Queue functions):

func Test_SomeTests(t *testing.T) {
	frostutil.QueueLayoutTest(t, test_SomeLayoutTest)
	frostutil.QueueUpdateTest(t, test_SomeUpdateTest)
	frostutil.QueueDrawTest(t, test_SomeDrawTest)	
}

func test_SomeLayoutTest(t *testing.T, outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
	// Your actual test here. Also return screen dimensions for the Layout function to return:
	return outsideWidth, outsideHeight // you can return something else if you like
}

func test_SomeUpdateTest(t *testing.T) {
	// Your actual test here
}

func test_SomeDrawTest(t *testing.T, screen *ebiten.Image) {
	// Your actual test here
}

You can have multiple tests per file, of course, and they can each queue as many things as they want.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Abs

func Abs[T int | int8 | int16 | int32 | int64 | float32 | float64](x T) T

Abs returns the absolute value of x.

func CenterString added in v1.2.0

func CenterString(str string, desiredLen int) (ret string)

CenterString horizontally centers a string by padding it with spaces, if its length is less than desiredLen. If len(str) < desiredLen, the returned string will have a length of desiredLen, and either have an equal number of spaces on both sides, or one additional space on the right side. If len(str) >= desiredLen, then str will be returned and no padding will have been added.

func Compose

func Compose[T any](f func(T) T, g func(T) T) func(T) T

Compose takes two functions f and g with a single parameter and return value of the same type, and returns a function that returns f(g(t)), where t is a parameter of that shared type.

func CopyImage

func CopyImage(img image.Image, mipmaps bool) (ret image.Image)

CopyImage creates a new image with the same width and height as img, and copies its pixel data into it. If img is an *ebiten.Image, it creates another *ebiten.Image and uses ReadPixels and WritePixels to copy the image data. If it's an *image.RGBA, it creates another *image.RGBA and directly copies the pixel data. If it's an *image.NRGBA, it creates another *image.NRGBA and directly copies the pixel data. If it's an *image.RGBA or *image.NRGBA and the strides on the input and output images are different (which shouldn't happen unless Go's image code changes to make stride something other than width * 4 in RGBA and NRGBA images), then it calls CopyImageLines, which is still reasonably fast. If it's any other image type, it creates an *image.RGBA and calls SlowImageCopy, which copies the image data from the input image into the output image pixel by pixel using At and Set, which is pretty slow. CopyImage returns the copy it creates.

func CopyImageLines

func CopyImageLines(oPix []byte, oStride int, iPix []byte, iStride int)

CopyImageLines copies pixel data from iPix to oPix line by line. oPix should be the output image's pixel data buffer, oStride should be its Stride, and iPix and iStride should be the same for the input image.

func DegreesToRadians

func DegreesToRadians(deg float64) (rad float64)

Converts degrees to radians

func EscapeStr

func EscapeStr(x string, sep rune) string

EscapeStr escapes '\n' and '<sep>' into "\\n" and "\<sep>".

func Join

func Join(xs []string, sep rune) string

Joins a set of strings, placing separators (sep) between them, escaping any separators (sep) or endlines in xs (turning '\n's into "\\n"s). The joined/modified string is returned, and the original slice of strings is unmodified.

func Map

func Map[T, S any](f func(T) S, xs []T) (ys []S)

Map returns a new slice ys created from the results of calling the function f on each element x of the slice xs. If you have a function that takes multiple arguments, you can use Map by using partial application, which you can do by writing a function (let's call it "foo") that takes your multi-argument function (let's call it "bar") along with the other arguments, and which returns a function that takes a single argument and calls bar with that argument along with the arguments which you passed to foo, and returns whatever bar returns. e.g.

 func AddThree(x, y, z int) int { return x + y + z }
 func PartialAddThree(y, z int) func(int) int {
	return func(x int) int {
		return AddThree(x, y, z)
	}
 }
 Map(PartialAddThree(10, 100), []int {1, 2, 3, 4, 5})

There's an example of partial application implemented as a generic function in Partial2to1, but it is probably harder to understand than the example above.

func MatchesImage added in v1.1.0

func MatchesImage(t *testing.T, imageName string, img image.Image) bool

MatchesImage compares an image.Image to "testdata/expected/<imageName>.png". If img is not nil, it attempts to open "testdata/expected/<imageName>.png". If it succeeds, it converts it to an image.Image, and then compares the two images. If it fails, it writes the image to "testdata/failed/<imageName>.png" and raises a test failure. It can handle *ebiten.Images and save them as PNGs. Also returns true if the images match, and false if they don't.

func Max

func Max[T uint | int | uint8 | uint16 | uint32 | uint64 | int8 | int16 | int32 | int64 | float32 | float64](x, y T) T

Max returns whichever of x or y is highest.

func Min

func Min[T uint | int | uint8 | uint16 | uint32 | uint64 | int8 | int16 | int32 | int64 | float32 | float64](x, y T) T

Min returns whichever of x or y is lowest.

func MultiplyAlphaBytes

func MultiplyAlphaBytes(red, green, blue, alpha byte) (r, g, b, a byte)

MultiplyAlphaBytes converts NRGBA bytes to RGBA bytes. Currently, it just packs the bytes into a color.NRGBA, calls RGBA() on it, and right-bitshifts each returned uint32 by 8, returning them as bytes (since RGBA() returns 16-bit numbers in uint32s). Note: Because this calls RGBA(), it loses the color components when alpha is zero.

func MultiplyAlphaBytesPreserveColors

func MultiplyAlphaBytesPreserveColors(red, green, blue, alpha byte) (r, g, b, a byte)

MultiplyAlphaBytesPreserveColors converts NRGBA bytes to RGBA bytes, preserving the color components when the alpha component is zero. Because it preserves the color components, it gives different results from the standard color model conversion methods and from calling RGBA() on an NRGBA color when the alpha value is 0.

func NewEImageFromImage

func NewEImageFromImage(img image.Image, mipmaps bool) (ret *ebiten.Image)

NewEImageFromImage converts an image.Image to an *ebiten.Image by creating a new *ebiten.Image and writing the image data into it (with the new WritePixels method introduced in ebitengine 2.4.*). If mipmaps is true, the *ebiten.Image is created with mipmaps. It can relatively quickly handle the source image being an *ebiten.Image, an *image.RGBA, or an *image.NRGBA (in which case it converts the NRGBA pixel data to RGBA pixel data, since that is what *ebiten.Images use). It can handle other image types, but does it more slowly since it has to copy the image data pixel by pixel. I originally wrote this because ebiten.NewImageFromImage was corrupting the pixel data of the source images passed to it (I don't know if it still does, but if so, calling this instead should prevent it).

func NewImageFromEImage

func NewImageFromEImage(eImg *ebiten.Image) (img *image.RGBA)

NewImageFromEImage converts an *ebiten.Image to an *image.RGBA by retrieving the raw RGBA pixel data and copying it to a new image, which it returns. We do this so that we can save *ebiten.Images as PNGs, since attempting to directly feed an *ebiten.Image to png.Encode results in garbage output.

func OnTestMain added in v1.1.0

func OnTestMain(m *testing.M)

This has to be called from a TestMain(m *testing.M) function in any package that uses QueueUpdateTest, QueueDrawTest, or QueueLayoutTest. It sets up and runs Ebitengine, runs your test functions (via m.Run) which should call Queue*Test, waits for it to finish, and then closes the channels and sets their variables to nil, which prompts Update to tell Ebitengine to shut down.

func Partial2to1

func Partial2to1[T, S, R any](f func(T, R) S, r R) func(T) S

Partial application from two parameters to one. Currently this is only here for example purposes. There would need to be one of these functions for each combination of input and output function parameter counts for which partial application was needed, so it is probably more useful to write functions like this on a case by case basis in the rare event that one is needed. Perhaps the most likely reason one would be needed would be to be able to use the Map function with a function that takes more than one argument.

func QueueDrawTest added in v1.1.0

func QueueDrawTest(t *testing.T, f func(t *testing.T, screen *ebiten.Image))

QueueDrawTest checks to make sure OnTestMain was called, and if it was, it packages up the parameters t and f, and sends them through the drawTests channel for Draw. It waits for Draw to let it know that it has finished running f(t, screen), and then returns. If OnTestMain was never called, it triggers a test failure and warns that you need to call OnTestMain from TestMain in every package which contains calls to QueueDrawTest.

func QueueLayoutTest added in v1.1.0

func QueueLayoutTest(t *testing.T, f func(t *testing.T, outsideWidth, outsideHeight int) (screenWidth, screenHeight int))

QueueLayoutTest checks to make sure OnTestMain was called, and if it was, it packages up the parameters t and f, and sends them through the layoutTests channel for Layout. It waits for Layout to let it know that it has finished running f(t, outsideWidth, outsideHeight), and then returns. If OnTestMain was never called, it triggers a test failure and warns that you need to call OnTestMain from TestMain in every package which contains calls to QueueLayoutTest.

func QueueUpdateTest added in v1.1.0

func QueueUpdateTest(t *testing.T, f func(t *testing.T))

QueueUpdateTest checks to make sure OnTestMain was called, and if it was, it packages up the parameters t and f, and sends them through the updateTests channel for Update. It waits for Update to let it know that it has finished running f(t), and then returns. If OnTestMain was never called, it triggers a test failure and warns that you need to call OnTestMain from TestMain in every package which contains calls to QueueUpdateTest.

func RadiansToDegrees

func RadiansToDegrees(rad float64) (deg float64)

Converts radians to degrees

func SearchStringsUnsorted

func SearchStringsUnsorted(ax []string, x string, defaultValue int) int

SearchStringsUnsorted searches for x in an unsorted slice of strings and returns its index. If x is not in ax, it returns defaultValue.

func SlowImageCopy

func SlowImageCopy(oImg, iImg image.Image) (err error)

SlowImageCopy copies pixel data from iImg to oImg pixel by pixel using (Image).At and (Image).Set. It's called by CopyImage or NewEImageFromImage if iImg isn't an *ebiten.Image, *image.NRGBA, or *image.RGBA. Currently, oImg must still be one of those three for this to work, since the image.Image interface doesn't have a Set method. If it isn't one of those, this returns an error. For each pixel of each row, it uses the At method to get the pixel color from the source image, and Set to set it on the output image. Warning: (*image.NRGBA).Set sets the pixel's color components to 0 when the alpha component is 0, even if the color components aren't 0 in the color which was returned by At. This causes any tests which attempt to use At and Set to copy pixels with non-zero color components and a zero alpha component to show failures. The same is true for *(image.NRGBA64).Set.

func Split

func Split(s string, sep rune) (out []string)

Split splits a string by the separator sep, but does not split it where a separator is escaped (prefixed with a \). It unescapes escaped separators (removes the \ before them) and turns "\\n"s into '\n's in the output (that is, it unescapes endlines).

func ToNRGBA

func ToNRGBA(c color.Color) (r, g, b, a byte)

ToNRGBA converts a color to 8-bit RGBA values which are not premultiplied, unlike color.RGBA(). This has special fast code for color.NRGBA, color.NRGBA64, color.Gray, color.Gray16, color.Alpha, and color.Alpha16, since none of those are premultiplied. For RGBA and RGBA64, it calls our UnmultiplyAlpha function, which both un-premultiplies the alpha from the RGB components, and reduces the color to 8bpp. UnmultiplyAlpha only un-premultiplies when the alpha returned by c.RGBA() is > 0 and < 0xffff.

func ToNRGBA_Color

func ToNRGBA_Color(c color.Color) (out color.Color)

ToNRGBA_Color runs c through ToNRGBA and then packages its output into a color.NRGBA, which it returns.

func ToNRGBA_U32

func ToNRGBA_U32(c color.Color) (u uint32)

ToNRGBA_U32 runs c through ToNRGBA and then packages its output into a uint32 where the highest byte is red, the next highest is green, the third highest is blue, and the lowest byte is alpha.

func UnescapeStr

func UnescapeStr(x string, sep rune) string

UnescapeStr unescapes "\\n" and "\<sep>" back into '\n' and '<sep>'.

func UnmultiplyAlpha

func UnmultiplyAlpha(c color.Color) (r, g, b, a byte)

UnmultiplyAlpha returns a color's RGBA components as 8-bit integers by calling c.RGBA() and then removing the alpha premultiplication (if present), and finally bitshifting each component right by 8 (>> 8) to reduce it from the 16-bit component output of RGBA() to 8-bit component output. The un-premultiplication is skipped if the alpha returned by c.RGBA() is 0 or 0xffff. This preserves non-zero color components when the alpha value is zero, unlike color.NRGBAModel.Convert. The standard model conversion function and NRGBA colors' RGBA() method erase the color information when alpha is zero, so this capability is only relevant if you are manually editing the image's pixel buffer. If you want to preserve color information when encoding from NRGBA to RGBA, you can use MultiplyAlphaBytesPreserveColors.

func UnmultiplyAlphaBytes

func UnmultiplyAlphaBytes(red, green, blue, alpha byte) (r, g, b, a byte)

UnmultiplyAlphaBytes converts alpha-premultiplied RGBA bytes to NRGBA bytes by removing the alpha premultiplication. It's for use when directly converting bytes in an image's Pix buffer. This preserves non-zero color components when the alpha component is zero, unlike color.NRGBAModel.Convert. The standard model conversion function and NRGBA colors' RGBA() method erase the color information when alpha is zero, so this capability is only relevant if you are manually editing the image's pixel buffer. If you want to preserve color information when encoding from NRGBA to RGBA, you can use MultiplyAlphaBytesPreserveColors.

func Xor

func Xor(a, b bool) (c bool)

returns a xor b, because Go lacks a logical xor operator

Types

type DrawTest added in v1.1.0

type DrawTest struct {
	// contains filtered or unexported fields
}

DrawTest pointers are sent through a channel from QueueDrawTest to *TestGame.Draw.

type DrawTestFunc added in v1.1.0

type DrawTestFunc func(t *testing.T, screen *ebiten.Image)

Any test function meant to run in Draw must have this signature

type LayoutTest added in v1.1.0

type LayoutTest struct {
	// contains filtered or unexported fields
}

LayoutTest pointers are sent through a channel from QueueLayoutTest to *TestGame.Layout.

type LayoutTestFunc added in v1.1.0

type LayoutTestFunc func(t *testing.T, outsideWidth, outsideHeight int) (screenWidth, screenHeight int)

Any test function meant to run in Layout must have this signature

type TestGame added in v1.1.0

type TestGame struct {
	// contains filtered or unexported fields
}

TestGame contains the Update, Layout, and Draw methods that Ebitengine calls.

func (*TestGame) Draw added in v1.1.0

func (game *TestGame) Draw(screen *ebiten.Image)

Each time Draw is called by Ebitengine, it retrieves a draw test, if any are queued, from the drawTests channel, runs it, and then lets QueueDrawTest know that it has finished running it (so that it will return). If drawTests is nil, it does nothing.

func (*TestGame) Layout added in v1.1.0

func (game *TestGame) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int)

Each time Layout is called by Ebitengine, it retrieves a layout test, if any are queued, from the layoutTests channel, runs it, records the screenWidth and screenHeight that it returns, and then lets QueueLayoutTest know that it has finished running it (so that it will return). If layoutTests is nil, it does nothing. It returns the screenWidth and screenHeight returned by the last layout test, or 1920 and 1080 if no layout tests were ever queued.

func (*TestGame) Update added in v1.1.0

func (game *TestGame) Update() (err error)

Each time Update is called by Ebitengine, it retrieves an update test, if any are queued, from the updateTests channel, runs it, and then lets QueueUpdateTest know that it has finished running it (so that it will return). If updateTests is nil, then it returns an error to tell Ebitengine to shut down.

type UpdateTest added in v1.1.0

type UpdateTest struct {
	// contains filtered or unexported fields
}

UpdateTest pointers are sent through a channel from QueueUpdateTest to *TestGame.Update.

type UpdateTestFunc added in v1.1.0

type UpdateTestFunc func(t *testing.T)

Any test function meant to run in Update must have this signature

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL