178 lines
4.5 KiB
Go
178 lines
4.5 KiB
Go
package assets
|
|
|
|
import (
|
|
"bytes"
|
|
"embed"
|
|
"encoding/json"
|
|
"image"
|
|
_ "image/png"
|
|
"io/fs"
|
|
"log"
|
|
"log/slog"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hajimehoshi/ebiten/v2"
|
|
)
|
|
|
|
// Maps image name to image data
|
|
type AssetRegistry map[string]*ebiten.Image
|
|
|
|
// A helper to pass the registry easier around
|
|
type assetData struct {
|
|
Registry AssetRegistry
|
|
}
|
|
|
|
// to parse asesprite animation json
|
|
type AnimationGeo struct {
|
|
X int `json:"x"`
|
|
Y int `json:"y"`
|
|
Width int `json:"w"`
|
|
Height int `json:"h"`
|
|
}
|
|
|
|
type AnimationFrame struct {
|
|
Position AnimationGeo `json:"frame"`
|
|
Duration int `json:"duration"`
|
|
}
|
|
|
|
type AnimationMeta struct {
|
|
Name string `json:"image"`
|
|
Geo AnimationGeo `json:"size"`
|
|
}
|
|
|
|
// Needed to thaw sprite set written by asesprite
|
|
type AnimationJSON struct {
|
|
Meta AnimationMeta `json:"Meta"`
|
|
Frames map[string]AnimationFrame `json:"frames"`
|
|
}
|
|
|
|
// used to store Animation data
|
|
type AnimationSprite struct {
|
|
Sprite *ebiten.Image
|
|
Duration time.Duration
|
|
}
|
|
|
|
type AnimationSet struct {
|
|
Width, Height int
|
|
Sprites []AnimationSprite
|
|
File string
|
|
}
|
|
|
|
// names in the registry match the sprite set png file name, the JSON
|
|
// file names are irrelevant as they point to the matching file using
|
|
// the "image" field.
|
|
type AnimationRegistry map[string]AnimationSet
|
|
|
|
//go:embed sprites/*.png fonts/*.ttf levels/*.ldtk shaders/*.kg sprites/*.json
|
|
var assetfs embed.FS
|
|
|
|
// Called at build time, creates the global asset and animation registries
|
|
var Assets, Animations = LoadImages()
|
|
|
|
// load pngs and json files
|
|
func LoadImages() (AssetRegistry, AnimationRegistry) {
|
|
dir := "sprites"
|
|
imagedata := &assetData{}
|
|
imagedata.Registry = AssetRegistry{}
|
|
rawanimations := []AnimationJSON{}
|
|
|
|
// we use embed.FS to iterate over all files in ./assets/
|
|
entries, err := assetfs.ReadDir(dir)
|
|
if err != nil {
|
|
log.Fatalf("failed to read assets dir %s: %s", dir, err)
|
|
}
|
|
|
|
for _, imagefile := range entries {
|
|
path := path.Join(dir, imagefile.Name())
|
|
|
|
fd, err := assetfs.Open(path)
|
|
if err != nil {
|
|
log.Fatalf("failed to open file %s: %s", imagefile.Name(), err)
|
|
}
|
|
defer fd.Close()
|
|
|
|
switch {
|
|
case strings.HasSuffix(path, ".png"):
|
|
name, image := ReadImage(imagefile, fd)
|
|
imagedata.Registry[name] = image
|
|
|
|
case strings.HasSuffix(path, ".json"):
|
|
animationjson := ReadJson(imagefile, fd)
|
|
rawanimations = append(rawanimations, animationjson)
|
|
}
|
|
|
|
slog.Debug("loaded asset", "path", path)
|
|
}
|
|
|
|
// preprocess animation sprites
|
|
animations := ProcessAnimations(rawanimations, imagedata)
|
|
|
|
return imagedata.Registry, animations
|
|
}
|
|
|
|
func ReadImage(imagefile fs.DirEntry, fd fs.File) (string, *ebiten.Image) {
|
|
name := strings.TrimSuffix(imagefile.Name(), ".png")
|
|
|
|
img, _, err := image.Decode(fd)
|
|
if err != nil {
|
|
log.Fatalf("failed to decode image %s: %s", imagefile.Name(), err)
|
|
}
|
|
|
|
image := ebiten.NewImageFromImage(img)
|
|
|
|
return name, image
|
|
}
|
|
|
|
func ReadJson(imagefile fs.DirEntry, fd fs.File) AnimationJSON {
|
|
buf := new(bytes.Buffer)
|
|
buf.ReadFrom(fd)
|
|
|
|
animationjson := AnimationJSON{}
|
|
|
|
err := json.Unmarshal(buf.Bytes(), &animationjson)
|
|
if err != nil {
|
|
log.Fatalf("failed to parse JSON in %s: %s", imagefile, err)
|
|
}
|
|
|
|
return animationjson
|
|
}
|
|
|
|
// turn a raw JSON from Asesprite into something we can use better
|
|
// internally we also load all the sprites of an animation by using
|
|
// image.SubImage() on the spriteset matching the animation name.
|
|
// so, if the animation JSON is called "player-idle.json", then we
|
|
// expect to receive the spriteset in a file "player-idle.png", which
|
|
// has of course already been loaded at this stage. These spritesets
|
|
// must contain a row of sprites. We get the measurements from the JSON.
|
|
func ProcessAnimations(rawanimations []AnimationJSON, imagedata *assetData) AnimationRegistry {
|
|
animations := AnimationRegistry{}
|
|
|
|
for _, animation := range rawanimations {
|
|
animationset := AnimationSet{}
|
|
|
|
animationset.File = strings.TrimSuffix(animation.Meta.Name, ".png")
|
|
|
|
for _, frame := range animation.Frames {
|
|
sprite := imagedata.Registry[animationset.File].SubImage(
|
|
image.Rect(
|
|
frame.Position.X,
|
|
frame.Position.Y,
|
|
frame.Position.X+frame.Position.Width,
|
|
frame.Position.Y+frame.Position.Height,
|
|
)).(*ebiten.Image)
|
|
|
|
animationset.Sprites = append(animationset.Sprites,
|
|
AnimationSprite{Sprite: sprite, Duration: time.Duration(frame.Duration)})
|
|
}
|
|
|
|
animationset.Width = animationset.Sprites[0].Sprite.Bounds().Dx()
|
|
animationset.Height = animationset.Sprites[0].Sprite.Bounds().Dy()
|
|
|
|
animations[animationset.File] = animationset
|
|
}
|
|
|
|
return animations
|
|
}
|