doodle/pkg/wallpaper/wallpaper.go
Noah Petherbridge 215ed5c847 Stabilize Load Screen by Deferring SDL2 Calls
* The loading screen for Edit and Play modes is stable and the risk of
  game crash is removed. The root cause was the setupAsync() functions
  running on a background goroutine, and running SDL2 draw functions
  while NOT on the main thread, which causes problems.
* The fix is all SDL2 Texture draws become lazy loaded: when the main
  thread is presenting, any Wallpaper or ui.Image that has no texture
  yet gets one created at that time from the cached image.Image.
* All internal game logic then uses image.Image types, to cache bitmaps
  of Level Chunks, Wallpaper images, Sprite icons, etc. and the game is
  free to prepare these asynchronously; only the main thread ever
  Presents and the SDL2 textures initialize on first appearance.
* Several functions had arguments cleaned up: Canvas.LoadLevel() does
  not need the render.Engine as (e.g. wallpaper) textures don't render
  at that stage.
2021-07-19 17:14:00 -07:00

191 lines
4.6 KiB
Go

package wallpaper
import (
"bytes"
"encoding/base64"
"image"
"image/draw"
"os"
"path/filepath"
"runtime"
"strings"
"git.kirsle.net/apps/doodle/assets"
"git.kirsle.net/apps/doodle/pkg/filesystem"
"git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/go/render"
)
// Wallpaper is a repeatable background image to go behind levels.
type Wallpaper struct {
Name string
Format string // image file format
Image *image.RGBA
// Ready status is set to true if the wallpaper loaded itself properly.
// Notably in WASM, wallpapers don't load currently.
ready bool
// Parsed values.
quarterWidth int
quarterHeight int
// The four parsed images.
corner *image.RGBA // Top Left corner
top *image.RGBA // Top repeating
left *image.RGBA // Left repeating
repeat *image.RGBA // Main repeating
// Cached textures.
tex struct {
corner render.Texturer
top render.Texturer
left render.Texturer
repeat render.Texturer
}
}
// FromImage creates a Wallpaper from an image.Image.
// If the renger.Engine is nil it will compute images but not pre-cache any
// textures yet.
func FromImage(img *image.RGBA, name string) (*Wallpaper, error) {
wp := &Wallpaper{
Name: name,
Image: img,
}
wp.cache()
return wp, nil
}
// FromFile creates a Wallpaper from a file on disk.
// If the renger.Engine is nil it will compute images but not pre-cache any
// textures yet.
func FromFile(filename string, embeddable filesystem.Embeddable) (*Wallpaper, error) {
// Default object to return on errors.
var defaultWP = &Wallpaper{
Name: strings.Split(filepath.Base(filename), ".")[0],
ready: false,
}
// WASM: no support yet for wallpapers.
if runtime.GOOS == "js" {
return defaultWP, nil
}
// Try and get an image object by any means.
var (
img image.Image
format string
imgErr error
)
// Try the level embedded files, then bindata, then filesystem.
if data, err := embeddable.GetFile(filename); err == nil {
log.Debug("wallpaper.FromFile(%s): found in embedded level files", filename)
bin, _ := base64.StdEncoding.DecodeString(string(data))
img, format, imgErr = image.Decode(bytes.NewReader(bin))
} else if data, err := assets.Asset(filename); err == nil {
log.Debug("wallpaper.FromFile(%s): found in program bindata", filename)
fh := bytes.NewBuffer(data)
img, format, imgErr = image.Decode(fh)
} else {
log.Debug("wallpaper.FromFile(%s): opening from disk", filename)
fh, err := os.Open(filename)
if err != nil {
return defaultWP, err
}
img, format, imgErr = image.Decode(fh)
}
// Image loading error?
if imgErr != nil {
return defaultWP, imgErr
}
// Ugly hack: make it an image.RGBA because the thing we get tends to be
// an image.Paletted, UGH!
var b = img.Bounds()
rgba := image.NewRGBA(b)
for x := b.Min.X; x < b.Max.X; x++ {
for y := b.Min.Y; y < b.Max.Y; y++ {
rgba.Set(x, y, img.At(x, y))
}
}
wp := &Wallpaper{
Name: strings.Split(filepath.Base(filename), ".")[0],
Format: format,
Image: rgba,
ready: true,
}
wp.cache()
return wp, nil
}
// cache the bitmap images.
func (wp *Wallpaper) cache() {
// Zero-bound the rect cuz an image.Rect doesn't necessarily contain 0,0
var rect = wp.Image.Bounds()
if rect.Min.X < 0 {
rect.Max.X += rect.Min.X
rect.Min.X = 0
}
if rect.Min.Y < 0 {
rect.Max.Y += rect.Min.Y
rect.Min.Y = 0
}
// Our quarter rect size.
wp.quarterWidth = int(float64((rect.Max.X - rect.Min.X) / 2))
wp.quarterHeight = int(float64((rect.Max.Y - rect.Min.Y) / 2))
quarter := image.Rect(0, 0, wp.quarterWidth, wp.quarterHeight)
// Slice the image into the four corners.
slice := func(dx, dy int) *image.RGBA {
slice := image.NewRGBA(quarter)
draw.Draw(
slice,
image.Rect(0, 0, wp.quarterWidth, wp.quarterHeight),
wp.Image,
image.Point{dx, dy},
draw.Over,
)
return slice
}
wp.corner = slice(0, 0)
wp.top = slice(wp.quarterWidth, 0)
wp.left = slice(0, wp.quarterHeight)
wp.repeat = slice(wp.quarterWidth, wp.quarterHeight)
}
// QuarterSize returns the width and height of the quarter images.
func (wp *Wallpaper) QuarterSize() (int, int) {
return wp.quarterWidth, wp.quarterHeight
}
// QuarterRect returns a Rect of the size of the quarter images.
func (wp *Wallpaper) QuarterRect() render.Rect {
return render.NewRect(wp.QuarterSize())
}
// Corner returns the top left corner image.
func (wp *Wallpaper) Corner() *image.RGBA {
return wp.corner
}
// Top returns the top repeating image.
func (wp *Wallpaper) Top() *image.RGBA {
return wp.top
}
// Left returns the left repeating image.
func (wp *Wallpaper) Left() *image.RGBA {
return wp.left
}
// Repeat returns the main repeating image.
func (wp *Wallpaper) Repeat() *image.RGBA {
return wp.repeat
}