doodle/pkg/wallpaper/wallpaper.go
Noah Petherbridge 4469847c72 Giant Screenshot Feature
In the Level Editor, the "Level->Giant Screenshot" menu will take a full
scale PNG screenshot of the entire level, with its wallpaper and
doodads, and save it in ~/.config/doodle/screenshots.

It is currently CPU intensive and slow. With future work it should be
made asynchronous. The function is abstracted away nicely so that the
doodad CLI tool may support this as well.
2021-10-03 17:21:17 -07:00

190 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
}