diff --git a/assets/fonts/NotoSans-Regular.ttf b/assets/fonts/NotoSans-Regular.ttf new file mode 100644 index 0000000..10589e2 Binary files /dev/null and b/assets/fonts/NotoSans-Regular.ttf differ diff --git a/assets/shaders/row.kg b/assets/shaders/row.kg new file mode 100644 index 0000000..f41c7c6 --- /dev/null +++ b/assets/shaders/row.kg @@ -0,0 +1,13 @@ +//kage:unit pixels + +package main + +var Alife int + +func Fragment(_ vec4, pos vec2, _ vec4) vec4 { + if Alife == 1 { + return vec4(0.0) + } + + return vec4(1.0) +} diff --git a/assets/sprites/button-9slice1.png b/assets/sprites/button-9slice1.png new file mode 100644 index 0000000..c0731c2 Binary files /dev/null and b/assets/sprites/button-9slice1.png differ diff --git a/assets/sprites/button-9slice2.png b/assets/sprites/button-9slice2.png new file mode 100644 index 0000000..f2454f9 Binary files /dev/null and b/assets/sprites/button-9slice2.png differ diff --git a/assets/sprites/button-9slice3.png b/assets/sprites/button-9slice3.png new file mode 100644 index 0000000..ab324da Binary files /dev/null and b/assets/sprites/button-9slice3.png differ diff --git a/assets/sprites/checkbox-9slice1.png b/assets/sprites/checkbox-9slice1.png new file mode 100644 index 0000000..93d9a84 Binary files /dev/null and b/assets/sprites/checkbox-9slice1.png differ diff --git a/assets/sprites/checkbox-9slice2.png b/assets/sprites/checkbox-9slice2.png new file mode 100644 index 0000000..74adc1f Binary files /dev/null and b/assets/sprites/checkbox-9slice2.png differ diff --git a/assets/sprites/checkbox-9slice3.png b/assets/sprites/checkbox-9slice3.png new file mode 100644 index 0000000..ab324da Binary files /dev/null and b/assets/sprites/checkbox-9slice3.png differ diff --git a/assets/src/button-9slice.ase b/assets/src/button-9slice.ase new file mode 100644 index 0000000..8f00914 Binary files /dev/null and b/assets/src/button-9slice.ase differ diff --git a/assets/src/button-9slice1.ase b/assets/src/button-9slice1.ase new file mode 100644 index 0000000..fbb734f Binary files /dev/null and b/assets/src/button-9slice1.ase differ diff --git a/assets/src/checkbox-9slice.ase b/assets/src/checkbox-9slice.ase new file mode 100644 index 0000000..f5640a6 Binary files /dev/null and b/assets/src/checkbox-9slice.ase differ diff --git a/config.go b/config.go index 91fb021..f836129 100644 --- a/config.go +++ b/config.go @@ -26,6 +26,7 @@ type Config struct { StateGrid *Grid // a grid from a statefile Wrap bool // wether wraparound mode is in place or not ShowVersion bool + UseShader bool // to use a shader to render alife cells // for internal profiling ProfileFile string @@ -178,6 +179,7 @@ func ParseCommandline() (*Config, error) { pflag.BoolVarP(&config.Invert, "invert", "i", false, "invert colors (dead cell: black)") pflag.BoolVarP(&config.ShowEvolution, "show-evolution", "s", false, "show evolution tracks") pflag.BoolVarP(&config.Wrap, "wrap-around", "w", false, "wrap around grid mode") + pflag.BoolVarP(&config.UseShader, "use-shader", "k", false, "use shader for cell rendering") pflag.StringVarP(&config.ProfileFile, "profile-file", "", "", "enable profiling") pflag.BoolVarP(&config.ProfileDraw, "profile-draw", "", false, "profile draw method (default false)") diff --git a/go.mod b/go.mod index d93f07f..2837d0b 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,10 @@ require ( github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 // indirect github.com/ebitengine/hideconsole v1.0.0 // indirect github.com/ebitengine/purego v0.7.0 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/jezek/xgb v1.1.1 // indirect + github.com/tinne26/etxt v0.0.8 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect ) diff --git a/go.sum b/go.sum index 0fa22ed..25f5685 100644 --- a/go.sum +++ b/go.sum @@ -6,15 +6,21 @@ github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A= github.com/ebitengine/purego v0.7.0 h1:HPZpl61edMGCEW6XK2nsR6+7AnJ3unUxpTZBkkIXnMc= github.com/ebitengine/purego v0.7.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/hajimehoshi/ebiten/v2 v2.7.4 h1:X+heODRQ3Ie9F9QFjm24gEZqQd5FSfR9XuT2XfHwgf8= github.com/hajimehoshi/ebiten/v2 v2.7.4/go.mod h1:H2pHVgq29rfm5yeQ7jzWOM3VHsjo7/AyucODNLOhsVY= github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4= github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/tinne26/etxt v0.0.8 h1:rjb58jkMkapRGLmhBMWnT76E/nMTXC5P1Q956BRZkoc= +github.com/tinne26/etxt v0.0.8/go.mod h1:QM/hlNkstsKC39elTFNKAR34xsMb9QoVosf+g9wlYxM= golang.org/x/image v0.16.0 h1:9kloLAKhUufZhA12l5fwnx2NZW39/we1UhBesW433jw= golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= diff --git a/loader-fonts.go b/loader-fonts.go new file mode 100644 index 0000000..7ba5e28 --- /dev/null +++ b/loader-fonts.go @@ -0,0 +1,99 @@ +package main + +import ( + "log" + + "github.com/golang/freetype/truetype" + "github.com/tinne26/etxt" + "golang.org/x/image/font" +) + +var FontRenderer = LoadFonts("assets/fonts") + +const ( + GameFont string = "NotoSans-Regular" + FontSizeBig int = 48 + FontSizeNormal int = 24 + FontSizeSmall int = 12 +) + +type Texter struct { + Renderer *etxt.Renderer + FontNormal *font.Face + FontBig *font.Face + FontSmall *font.Face +} + +func LoadFonts(dir string) Texter { + fontbytes, err := assetfs.ReadFile(dir + "/" + GameFont + ".ttf") + if err != nil { + log.Fatal(err) + } + + gamefont, err := truetype.Parse(fontbytes) + if err != nil { + log.Fatal(err) + } + + gameface := truetype.NewFace(gamefont, &truetype.Options{ + Size: float64(FontSizeNormal), + DPI: 72, + Hinting: font.HintingFull, + }) + + biggameface := truetype.NewFace(gamefont, &truetype.Options{ + Size: float64(FontSizeBig), + DPI: 72, + Hinting: font.HintingFull, + }) + + smallgameface := truetype.NewFace(gamefont, &truetype.Options{ + Size: float64(FontSizeSmall), + DPI: 72, + Hinting: font.HintingFull, + }) + + fontlib := etxt.NewFontLibrary() + _, _, err = fontlib.ParseEmbedDirFonts(dir, assetfs) + if err != nil { + log.Fatalf("Error while loading fonts: %s", err.Error()) + } + + if !fontlib.HasFont(GameFont) { + log.Fatal("missing font: " + GameFont) + } + + err = fontlib.EachFont(checkMissingRunes) + if err != nil { + log.Fatal(err) + } + + renderer := etxt.NewStdRenderer() + + glyphsCache := etxt.NewDefaultCache(10 * 1024 * 1024) // 10MB + renderer.SetCacheHandler(glyphsCache.NewHandler()) + renderer.SetFont(fontlib.GetFont(GameFont)) + + return Texter{ + Renderer: renderer, + FontNormal: &gameface, + FontBig: &biggameface, + FontSmall: &smallgameface, + } +} + +// helper function used with FontLibrary.EachFont to make sure +// all loaded fonts contain the characters or alphabet we want +func checkMissingRunes(name string, font *etxt.Font) error { + const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + const symbols = "0123456789 .,;:!?-()[]{}_&#@" + + missing, err := etxt.GetMissingRunes(font, letters+symbols) + if err != nil { + return err + } + if len(missing) > 0 { + log.Fatalf("Font '%s' missing runes: %s", name, string(missing)) + } + return nil +} diff --git a/loader-shaders.go b/loader-shaders.go new file mode 100644 index 0000000..e3d460f --- /dev/null +++ b/loader-shaders.go @@ -0,0 +1,49 @@ +package main + +import ( + "bytes" + "log" + "log/slog" + "path" + "strings" + + "github.com/hajimehoshi/ebiten/v2" +) + +type ShaderRegistry map[string]*ebiten.Shader + +var Shaders = LoadShaders("assets/shaders") + +func LoadShaders(dir string) ShaderRegistry { + shaders := ShaderRegistry{} + + entries, err := assetfs.ReadDir(dir) + if err != nil { + log.Fatalf("failed to read shaders dir %s: %s", dir, err) + } + + for _, file := range entries { + path := path.Join(dir, file.Name()) + fd, err := assetfs.Open(path) + if err != nil { + log.Fatalf("failed to open shader file %s: %s", file.Name(), err) + } + defer fd.Close() + + name := strings.TrimSuffix(file.Name(), ".kg") + + buf := new(bytes.Buffer) + buf.ReadFrom(fd) + + shader, err := ebiten.NewShader([]byte(buf.Bytes())) + if err != nil { + log.Fatal(err) + } + + shaders[name] = shader + + slog.Debug("loaded shader asset", "path", path) + } + + return shaders +} diff --git a/loader-sprites.go b/loader-sprites.go new file mode 100644 index 0000000..4ef3e50 --- /dev/null +++ b/loader-sprites.go @@ -0,0 +1,69 @@ +package main + +import ( + "embed" + "image" + _ "image/png" + "io/fs" + "log" + "path" + "strings" + + "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 +} + +//go:embed assets/sprites/*.png assets/fonts/*.ttf assets/shaders/*.kg +var assetfs embed.FS + +// Called at build time, creates the global asset and animation registries +var Assets = LoadImages("assets/sprites") + +// load pngs and json files +func LoadImages(dir string) AssetRegistry { + Registry := AssetRegistry{} + + // 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) + Registry[name] = image + } + } + + return Registry +} + +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 +}