doodle/pkg/wallpaper/wallpaper.go
Noah Petherbridge 640e75ba4d Custom Wallpapers for Levels
* You can now browse for a custom wallpaper image to use with your
  levels. A platform-native file picker dialog is used (no WASM support)
* In the New/Edit Level Properties dialog, the Wallpaper drop-down
  includes an option to browse for a custom map.
* When editing an existing level: the wallpaper takes effect immediately
  in your level once the file is picked. For NEW levels, the wallpaper
  will appear once the "Continue" button is pressed.
* All common image types supported: png, jpeg, gif.
* The wallpaper is embedded in the level using the filepath
  "assets/wallpapers/custom.b64img" as a Base64-encoded blob of the
  image data.
* The `doodad show` command will list the names and sizes of files
  embedded in levels. `doodad show --attachment <name>` will get an
  attachment and print it to the console window.
* To extract a wallpaper image from a level:
  `doodad show -a assets/wallpapers/custom.b64img | base64 -d > out.png`
2021-06-06 18:59:04 -07:00

186 lines
4.5 KiB
Go

package wallpaper
import (
"bytes"
"image"
"image/draw"
"os"
"path/filepath"
"runtime"
"strings"
"encoding/base64"
"git.kirsle.net/apps/doodle/pkg/bindata"
"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(e render.Engine, img *image.RGBA, name string) (*Wallpaper, error) {
wp := &Wallpaper{
Name: name,
Image: img,
}
wp.cache(e)
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(e render.Engine, 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 := bindata.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(e)
return wp, nil
}
// cache the bitmap images.
func (wp *Wallpaper) cache(e render.Engine) {
// 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
}
// 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
}