24 Commits

Author SHA1 Message Date
11fc54399c more zoom/center fixes. RLEs now load centered and visible. 2024-06-02 20:12:28 +02:00
80797a90fb fixed centering of squares, but not rectangles yet. 2024-06-02 19:42:06 +02:00
0bd7c51bfd fixed drawing bug: cells outside default canvas were not deleted 2024-06-02 19:28:56 +02:00
6afdf3fa62 fixed grid lines 2024-06-02 19:15:03 +02:00
f2289238df fixed initial cam pos, it's now always centered 2024-06-02 19:01:40 +02:00
37fb0d637f fixed clear screen problem, menus are now shown correctly. lots new
bugs though
2024-06-01 20:22:28 +02:00
9f1bdfd2af another try, but fails as well + added todos 2024-06-01 00:33:40 +02:00
507ac18853 toggle directly in scene's Draw, but fails too 2024-06-01 00:02:26 +02:00
8e821cbdc5 further clear screen debugging 2024-05-31 21:20:13 +02:00
320c666af9 added workaround for clearscreen problem, fixed grid line option 2024-05-31 14:19:30 +02:00
73be8b93f4 +todos 2024-05-30 19:47:37 +02:00
538216ea4d implemented basic menu and options structure, most stuff works 2024-05-30 19:45:13 +02:00
7ed0c83fcd disabled clear screen, added start of menu structure 2024-05-30 12:32:58 +02:00
5fae7256d7 refactored grid management 2024-05-30 10:23:31 +02:00
1ec4b9e257 refactored grid stuff, fixed font size calculation 2024-05-30 10:11:44 +02:00
56880014eb +tags support 2024-05-30 10:11:23 +02:00
dc966a8ddd tracks => traces 2024-05-30 10:11:01 +02:00
b8d6216feb added d key to toggle debugging 2024-05-28 13:39:02 +02:00
106ff1970a use etxt for debug printing 2024-05-28 13:37:32 +02:00
80ef96ceb7 prepare using ebitenui and shader, adding assets and assetloaders 2024-05-28 13:09:29 +02:00
99833745e6 fixed gmae speed control and mouse wheel handling, fasten zooming 2024-05-28 13:09:29 +02:00
8cb5585456 added caching for much better performance 2024-05-28 00:09:07 +02:00
ba247d0606 fix crash 2024-05-27 14:04:40 +02:00
aeebfb1997 add profiling support and window geom options to force geometry 2024-05-27 13:39:38 +02:00
28 changed files with 1188 additions and 207 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
golsky golsky
bak bak
dump* dump*
rect*
*profile

View File

@@ -23,12 +23,13 @@ PREFIX = /usr/local
UID = root UID = root
GID = 0 GID = 0
HAVE_POD := $(shell pod2text -h 2>/dev/null) HAVE_POD := $(shell pod2text -h 2>/dev/null)
#TAGS = -tags=ebitenginedebug
all: buildlocal all: buildlocal
buildlocal: buildlocal:
go build -o $(tool) go build $(TAGS) -o $(tool)
install: buildlocal install: buildlocal
install -d -o $(UID) -g $(GID) $(PREFIX)/bin install -d -o $(UID) -g $(GID) $(PREFIX)/bin

View File

@@ -18,7 +18,7 @@ Based on: https://youtu.be/FWSR_7kZuYg?si=ix1dmo76D8AmF25F
* flexible parameters as grid and cell size * flexible parameters as grid and cell size
* colors can be inverted * colors can be inverted
* evolution tracks can be shown, with age the cells color fades and * evolution traces can be shown, with age the cells color fades and
old life cells will be drawn in red old life cells will be drawn in red
* game grid lines can be enabled or disabled * game grid lines can be enabled or disabled
* game speed can be adjusted on startup and in-game * game speed can be adjusted on startup and in-game
@@ -61,7 +61,7 @@ Usage of ./golsky:
-p, --paused do not start simulation (use space to start) -p, --paused do not start simulation (use space to start)
-f, --rle-file string RLE pattern file -f, --rle-file string RLE pattern file
-r, --rule string game rule (default "B3/S23") -r, --rule string game rule (default "B3/S23")
-s, --show-evolution show evolution tracks -s, --show-evolution show evolution traces
-t, --ticks-per-generation int game speed: the higher the slower (default: 10) (default 10) -t, --ticks-per-generation int game speed: the higher the slower (default: 10) (default 10)
-v, --version show version -v, --version show version
-W, --width int grid width in cells (default 40) -W, --width int grid width in cells (default 40)
@@ -77,10 +77,12 @@ While it runs, there are a couple of commands you can use:
* page down: slow down * page down: slow down
* Mouse wheel: zoom in or out * Mouse wheel: zoom in or out
* move mouse while middle mouse button pressed: move canvas * move mouse while middle mouse button pressed: move canvas
* escape: reset to 1:1 zoom * r: reset to 1:1 zoom
* escape: open menu
* s: save game state to file (can be loaded with -l) * s: save game state to file (can be loaded with -l)
* c: enter copy mode. Mark a rectangle with the mouse, when you * c: enter copy mode. Mark a rectangle with the mouse, when you
release the mous button it is being saved to an RLE file release the mous button it is being saved to an RLE file
* d: toggle debug output
* q: quit * q: quit
# Report bugs # Report bugs

18
TODO.md
View File

@@ -1 +1,17 @@
- Implement RLE writing on scene.MarkDone - add all other options like size etc
- changing options mid-game has no effect in most cases, even after a restart
- Statefile loading does not work correclty anymore. With larger grids
everything is empty. With square grids part of the grid is cut
off. Smaller grids load though
- Also when loading a state file, centering doesn't work anymore, I
think the geom calculation is overthrown by the parser func. So, put
this calc into its own func and always call. Or - as stated below -
put it onto camera.go and call from Init().
- Zoom 0 on reset only works when world<screen. otherwise zoom would
be negative So, on Init() memoize centered camera position or add a
Center() function to camera.go. Then on reset calculate the zoom
level so that the world fits into the screen.

Binary file not shown.

13
assets/shaders/row.kg Normal file
View File

@@ -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)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

Binary file not shown.

245
config.go
View File

@@ -1,9 +1,13 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"log" "math"
"os" "os"
"runtime/pprof"
"strconv"
"strings"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"github.com/tlinden/golsky/rle" "github.com/tlinden/golsky/rle"
@@ -11,53 +15,171 @@ import (
// all the settings comming from commandline, but maybe tweaked later from the UI // all the settings comming from commandline, but maybe tweaked later from the UI
type Config struct { type Config struct {
Width, Height, Cellsize, Density int // measurements Width, Height, Cellsize, Density int // measurements
ScreenWidth, ScreenHeight int ScreenWidth, ScreenHeight int
TPG int // ticks per generation/game speed, 1==max TPG int // ticks per generation/game speed, 1==max
Debug, Empty, Invert, Paused bool // game modi Debug, Empty, Invert, Paused, Markmode bool // game modi
ShowEvolution, NoGrid, RunOneStep bool // flags ShowEvolution, ShowGrid, RunOneStep bool // flags
Rule *Rule // which rule to use, default: B3/S23 Rule *Rule // which rule to use, default: B3/S23
RLE *rle.RLE // loaded GOL pattern from RLE file RLE *rle.RLE // loaded GOL pattern from RLE file
Statefile string // load game state from it if non-nil Statefile string // load game state from it if non-nil
StateGrid *Grid // a grid from a statefile StateGrid *Grid // a grid from a statefile
Wrap bool // wether wraparound mode is in place or not Wrap bool // wether wraparound mode is in place or not
ShowVersion bool ShowVersion bool
UseShader bool // to use a shader to render alife cells
Restart, RestartGrid, RestartCache bool
StartWithMenu bool
Zoomfactor int
InitialCamPos []float64
DelayedStart bool // if true game, we wait. like pause but program induced
// for internal profiling
ProfileFile string
ProfileDraw bool
ProfileMaxLoops int64
} }
const ( const (
VERSION = "v0.0.6" VERSION = "v0.0.8"
Alive = 1 Alive = 1
Dead = 0 Dead = 0
DEFAULT_GRID_WIDTH = 600
DEFAULT_GRID_HEIGHT = 400
DEFAULT_CELLSIZE = 4
DEFAULT_ZOOMFACTOR = 150
DEFAULT_GEOM = "640x384"
) )
func GetRLE(filename string) *rle.RLE { // parse given window geometry and adjust game settings according to it
func (config *Config) ParseGeom(geom string) error {
// force a geom
geometry := strings.Split(geom, "x")
if len(geometry) != 2 {
return errors.New("failed to parse -g parameters, expecting WIDTHxHEIGHT")
}
width, err := strconv.Atoi(geometry[0])
if err != nil {
return errors.New("failed to parse width, expecting integer")
}
height, err := strconv.Atoi(geometry[1])
if err != nil {
return errors.New("failed to parse height, expecting integer")
}
config.ScreenWidth = width
config.ScreenHeight = height
config.Cellsize = DEFAULT_CELLSIZE
config.Zoomfactor = DEFAULT_ZOOMFACTOR
// calculate the initial cam pos. It is negative if the total grid
// size is smaller than the screen in a centered position, but
// it's zero if it's equal or larger than the screen.
config.InitialCamPos = make([]float64, 2)
config.InitialCamPos[0] = float64(((config.ScreenWidth - (config.Width * config.Cellsize)) / 2) * -1)
if config.Width*config.Cellsize >= config.ScreenWidth {
// must be positive if world wider than screen
config.InitialCamPos[0] = math.Abs(config.InitialCamPos[0])
}
if config.Height*config.Cellsize > config.ScreenHeight {
config.InitialCamPos[1] = math.Abs(float64(((config.ScreenHeight - (config.Height * config.Cellsize)) / 2)))
}
return nil
}
// check if we have been given an RLE file to load, then load it and
// adjust game settings accordingly
func (config *Config) ParseRLE(rlefile string) error {
if rlefile == "" {
return nil
}
rleobj, err := rle.GetRLE(rlefile)
if err != nil {
return err
}
if rleobj == nil {
return errors.New("failed to load RLE file (uncatched module error)")
}
config.RLE = rleobj
// adjust geometry if needed
if config.RLE.Width > config.Width || config.RLE.Height > config.Height {
config.Width = config.RLE.Width * 2
config.Height = config.RLE.Height * 2
config.Cellsize = config.ScreenWidth / config.Width
}
fmt.Printf("width: %d, screenwidth: %d, rlewidth: %d, cellsize: %d\n",
config.Width, config.ScreenWidth, config.RLE.Width, config.Cellsize)
// RLE needs an empty grid
config.Empty = true
// it may come with its own rule
if config.RLE.Rule != "" {
config.Rule = ParseGameRule(config.RLE.Rule)
}
return nil
}
// parse a state file, if given, and adjust game settings accordingly
func (config *Config) ParseStatefile() error {
if config.Statefile == "" {
return nil
}
grid, err := LoadState(config.Statefile)
if err != nil {
return fmt.Errorf("failed to load game state: %s", err)
}
config.Width = grid.Width
config.Height = grid.Height
config.Cellsize = config.ScreenWidth / config.Width
config.StateGrid = grid
return nil
}
func (config *Config) EnableCPUProfiling(filename string) error {
if filename == "" { if filename == "" {
return nil return nil
} }
content, err := os.ReadFile(filename) fd, err := os.Create(filename)
if err != nil { if err != nil {
log.Fatal(err) return err
} }
parsedRle, err := rle.Parse(string(content)) pprof.StartCPUProfile(fd)
if err != nil { defer pprof.StopCPUProfile()
log.Fatalf("failed to load RLE pattern file: %s", err)
}
return &parsedRle return nil
} }
func ParseCommandline() *Config { func ParseCommandline() (*Config, error) {
config := Config{} config := Config{}
var rule string var (
var rlefile string rule, rlefile, geom string
)
// commandline params, most configure directly config flags // commandline params, most configure directly config flags
pflag.IntVarP(&config.Width, "width", "W", 40, "grid width in cells") pflag.IntVarP(&config.Width, "width", "W", DEFAULT_GRID_WIDTH, "grid width in cells")
pflag.IntVarP(&config.Height, "height", "H", 40, "grid height in cells") pflag.IntVarP(&config.Height, "height", "H", DEFAULT_GRID_HEIGHT, "grid height in cells")
pflag.IntVarP(&config.Cellsize, "cellsize", "c", 8, "cell size in pixels") pflag.IntVarP(&config.Cellsize, "cellsize", "c", 8, "cell size in pixels")
pflag.StringVarP(&geom, "geom", "G", DEFAULT_GEOM, "window geometry in WxH in pixels, overturns -c")
pflag.IntVarP(&config.Density, "density", "D", 10, "density of random cells") pflag.IntVarP(&config.Density, "density", "D", 10, "density of random cells")
pflag.IntVarP(&config.TPG, "ticks-per-generation", "t", 10, pflag.IntVarP(&config.TPG, "ticks-per-generation", "t", 10,
"game speed: the higher the slower (default: 10)") "game speed: the higher the slower (default: 10)")
@@ -69,44 +191,33 @@ func ParseCommandline() *Config {
pflag.BoolVarP(&config.ShowVersion, "version", "v", false, "show version") pflag.BoolVarP(&config.ShowVersion, "version", "v", false, "show version")
pflag.BoolVarP(&config.Paused, "paused", "p", false, "do not start simulation (use space to start)") pflag.BoolVarP(&config.Paused, "paused", "p", false, "do not start simulation (use space to start)")
pflag.BoolVarP(&config.Debug, "debug", "d", false, "show debug info") pflag.BoolVarP(&config.Debug, "debug", "d", false, "show debug info")
pflag.BoolVarP(&config.NoGrid, "nogrid", "n", false, "do not draw grid lines") pflag.BoolVarP(&config.ShowGrid, "show-grid", "g", false, "draw grid lines")
pflag.BoolVarP(&config.Empty, "empty", "e", false, "start with an empty screen") pflag.BoolVarP(&config.Empty, "empty", "e", false, "start with an empty screen")
pflag.BoolVarP(&config.Invert, "invert", "i", false, "invert colors (dead cell: black)") 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.ShowEvolution, "show-evolution", "s", false, "show evolution traces")
pflag.BoolVarP(&config.Wrap, "wrap-around", "w", false, "wrap around grid mode") 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)")
pflag.Int64VarP(&config.ProfileMaxLoops, "profile-max-loops", "", 10, "how many loops to execute (default 10)")
pflag.Parse() pflag.Parse()
// check if we have been given an RLE file to load err := config.ParseGeom(geom)
config.RLE = GetRLE(rlefile) if err != nil {
if config.RLE != nil { return nil, err
if config.RLE.Width > config.Width || config.RLE.Height > config.Height {
config.Width = config.RLE.Width * 2
config.Height = config.RLE.Height * 2
fmt.Printf("rlew: %d, rleh: %d, w: %d, h: %d\n",
config.RLE.Width, config.RLE.Height, config.Width, config.Height)
}
// RLE needs an empty grid
config.Empty = true
// it may come with its own rule
if config.RLE.Rule != "" {
config.Rule = ParseGameRule(config.RLE.Rule)
}
} else if config.Statefile != "" {
grid, err := LoadState(config.Statefile)
if err != nil {
log.Fatalf("failed to load game state: %s", err)
}
config.Width = grid.Width
config.Height = grid.Height
config.StateGrid = grid
} }
config.ScreenWidth = config.Cellsize * config.Width err = config.ParseRLE(rlefile)
config.ScreenHeight = config.Cellsize * config.Height if err != nil {
return nil, err
}
err = config.ParseStatefile()
if err != nil {
return nil, err
}
// load rule from commandline when no rule came from RLE file, // load rule from commandline when no rule came from RLE file,
// default is B3/S23, aka conways game of life // default is B3/S23, aka conways game of life
@@ -114,5 +225,25 @@ func ParseCommandline() *Config {
config.Rule = ParseGameRule(rule) config.Rule = ParseGameRule(rule)
} }
return &config //repr.Println(config)
return &config, nil
}
func (config *Config) TogglePaused() {
config.Paused = !config.Paused
}
func (config *Config) ToggleDebugging() {
fmt.Println("DEBUG TOGGLED")
config.Debug = !config.Debug
}
func (config *Config) ToggleInvert() {
config.Invert = !config.Invert
config.RestartCache = true
}
func (config *Config) ToggleGridlines() {
config.ShowGrid = !config.ShowGrid
config.RestartCache = true
} }

40
game.go
View File

@@ -1,12 +1,16 @@
package main package main
import "github.com/hajimehoshi/ebiten/v2" import (
"github.com/hajimehoshi/ebiten/v2"
)
type Game struct { type Game struct {
ScreenWidth, ScreenHeight, Cellsize int ScreenWidth, ScreenHeight, ReadlWidth, Cellsize int
Scenes map[SceneName]Scene Scenes map[SceneName]Scene
CurrentScene SceneName CurrentScene SceneName
Config *Config Config *Config
Scale float32
Screen *ebiten.Image
} }
func NewGame(config *Config, startscene SceneName) *Game { func NewGame(config *Config, startscene SceneName) *Game {
@@ -20,12 +24,16 @@ func NewGame(config *Config, startscene SceneName) *Game {
// setup scene[s] // setup scene[s]
game.CurrentScene = startscene game.CurrentScene = startscene
game.Scenes[Play] = NewPlayScene(game, config) game.Scenes[Play] = NewPlayScene(game, config)
game.Scenes[Menu] = NewMenuScene(game, config)
game.Scenes[Options] = NewOptionsScene(game, config)
// setup environment // setup environment
ebiten.SetWindowSize(game.ScreenWidth, game.ScreenHeight) ebiten.SetWindowSize(game.ScreenWidth, game.ScreenHeight)
ebiten.SetWindowTitle("golsky - conway's game of life") ebiten.SetWindowTitle("golsky - conway's game of life")
ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled) ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled)
ebiten.SetScreenClearedEveryFrame(true)
game.Screen = ebiten.NewImage(game.ScreenWidth, game.ScreenHeight)
return game return game
} }
@@ -34,6 +42,8 @@ func (game *Game) GetCurrentScene() Scene {
} }
func (game *Game) Layout(outsideWidth, outsideHeight int) (int, int) { func (game *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
game.ReadlWidth = outsideWidth
game.Scale = float32(game.ScreenWidth) / float32(outsideWidth)
return game.ScreenWidth, game.ScreenHeight return game.ScreenWidth, game.ScreenHeight
} }
@@ -42,12 +52,8 @@ func (game *Game) Update() error {
scene.Update() scene.Update()
next := scene.GetNext() next := scene.GetNext()
if next != game.CurrentScene { if next != game.CurrentScene {
// make sure we stay on the selected scene
scene.ResetNext() scene.ResetNext()
// finally switch
game.CurrentScene = next game.CurrentScene = next
} }
@@ -55,13 +61,19 @@ func (game *Game) Update() error {
} }
func (game *Game) Draw(screen *ebiten.Image) { func (game *Game) Draw(screen *ebiten.Image) {
scene := game.GetCurrentScene() // first draw primary scene[s], although there are only 1
for current, scene := range game.Scenes {
if scene.IsPrimary() {
// primary scenes always draw
scene.Draw(screen)
if scene.Clearscreen() { if current == game.CurrentScene {
ebiten.SetScreenClearedEveryFrame(true) // avoid to redraw it in the next step
} else { return
ebiten.SetScreenClearedEveryFrame(false) }
}
} }
scene := game.GetCurrentScene()
scene.Draw(screen) scene.Draw(screen)
} }

5
go.mod
View File

@@ -13,7 +13,12 @@ require (
github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 // indirect github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 // indirect
github.com/ebitengine/hideconsole v1.0.0 // indirect github.com/ebitengine/hideconsole v1.0.0 // indirect
github.com/ebitengine/purego v0.7.0 // indirect github.com/ebitengine/purego v0.7.0 // indirect
github.com/ebitenui/ebitenui v0.5.6 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/jezek/xgb v1.1.1 // indirect github.com/jezek/xgb v1.1.1 // indirect
github.com/tinne26/etxt v0.0.8 // indirect
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
golang.org/x/sync v0.7.0 // indirect golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.20.0 // indirect golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
) )

12
go.sum
View File

@@ -6,15 +6,27 @@ 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/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 h1:HPZpl61edMGCEW6XK2nsR6+7AnJ3unUxpTZBkkIXnMc=
github.com/ebitengine/purego v0.7.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= github.com/ebitengine/purego v0.7.0/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
github.com/ebitenui/ebitenui v0.5.6 h1:qyJRU5j+lQo1lamxB48IBwMxMfz1xNb5iWUayCtA0Wk=
github.com/ebitenui/ebitenui v0.5.6/go.mod h1:I0rVbTOUi7gWKTPet2gzbvhOdkHp5pJXMM6c6b3dRoE=
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 h1:X+heODRQ3Ie9F9QFjm24gEZqQd5FSfR9XuT2XfHwgf8=
github.com/hajimehoshi/ebiten/v2 v2.7.4/go.mod h1:H2pHVgq29rfm5yeQ7jzWOM3VHsjo7/AyucODNLOhsVY= 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 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= 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 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 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=
github.com/tinne26/etxt v0.0.9-alpha.6.0.20240409152929-91bfc562becc h1:+USGSXbkrRAy6bz3Qm4GUczhqeXe7XlRfkRexCSFxkw=
github.com/tinne26/etxt v0.0.9-alpha.6.0.20240409152929-91bfc562becc/go.mod h1:Icbd4bDjrXag1oYIhB51CrkMYqRb7YMv0AsrOSfNKfU=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/image v0.16.0 h1:9kloLAKhUufZhA12l5fwnx2NZW39/we1UhBesW433jw= golang.org/x/image v0.16.0 h1:9kloLAKhUufZhA12l5fwnx2NZW39/we1UhBesW433jw=
golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs= 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 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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=

74
grid.go
View File

@@ -9,6 +9,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/tlinden/golsky/rle"
) )
type Grid struct { type Grid struct {
@@ -34,6 +36,7 @@ func NewGrid(width, height, density int, empty bool) *Grid {
return grid return grid
} }
// Create a new 1:1 instance
func (grid *Grid) Clone() *Grid { func (grid *Grid) Clone() *Grid {
newgrid := &Grid{} newgrid := &Grid{}
@@ -44,6 +47,16 @@ func (grid *Grid) Clone() *Grid {
return newgrid return newgrid
} }
// copy data
func (grid *Grid) Copy(other *Grid) {
for y := range grid.Data {
for x := range grid.Data[y] {
other.Data[y][x] = grid.Data[y][x]
}
}
}
// delete all contents
func (grid *Grid) Clear() { func (grid *Grid) Clear() {
for y := range grid.Data { for y := range grid.Data {
for x := range grid.Data[y] { for x := range grid.Data[y] {
@@ -52,7 +65,8 @@ func (grid *Grid) Clear() {
} }
} }
func (grid *Grid) FillRandom(game *ScenePlay) { // initialize with random life cells using the given density
func (grid *Grid) FillRandom() {
if !grid.Empty { if !grid.Empty {
for y := range grid.Data { for y := range grid.Data {
for x := range grid.Data[y] { for x := range grid.Data[y] {
@@ -64,16 +78,43 @@ func (grid *Grid) FillRandom(game *ScenePlay) {
} }
} }
func GetFilename(generations int64) string { func (grid *Grid) Dump() {
now := time.Now() for y := 0; y < grid.Height; y++ {
return fmt.Sprintf("dump-%s-%d.gol", now.Format("20060102150405"), generations) for x := 0; x < grid.Width; x++ {
if grid.Data[y][x] == 1 {
fmt.Print("XX")
} else {
fmt.Print(" ")
}
}
fmt.Println()
}
} }
func GetFilenameRLE(generations int64) string { // initialize using a given RLE pattern
now := time.Now() func (grid *Grid) LoadRLE(pattern *rle.RLE) {
return fmt.Sprintf("rect-%s-%d.rle", now.Format("20060102150405"), generations) if pattern != nil {
startX := (grid.Width / 2) - (pattern.Width / 2)
startY := (grid.Height / 2) - (pattern.Height / 2)
var y, x int
for rowIndex, patternRow := range pattern.Pattern {
for colIndex := range patternRow {
if pattern.Pattern[rowIndex][colIndex] > 0 {
x = colIndex + startX
y = rowIndex + startY
grid.Data[y][x] = 1
}
}
}
//grid.Dump()
}
} }
// save the contents of the whole grid as a simple mcell alike
// file. One line per row, 0 for dead and 1 for life cell.
func (grid *Grid) SaveState(filename string) error { func (grid *Grid) SaveState(filename string) error {
file, err := os.Create(filename) file, err := os.Create(filename)
if err != nil { if err != nil {
@@ -81,7 +122,7 @@ func (grid *Grid) SaveState(filename string) error {
} }
defer file.Close() defer file.Close()
for y, _ := range grid.Data { for y := range grid.Data {
for _, cell := range grid.Data[y] { for _, cell := range grid.Data[y] {
_, err := file.WriteString(strconv.FormatInt(cell, 10)) _, err := file.WriteString(strconv.FormatInt(cell, 10))
if err != nil { if err != nil {
@@ -94,6 +135,7 @@ func (grid *Grid) SaveState(filename string) error {
return nil return nil
} }
// the reverse of the above, load a mcell file
func LoadState(filename string) (*Grid, error) { func LoadState(filename string) (*Grid, error) {
fd, err := os.Open(filename) fd, err := os.Open(filename)
if err != nil { if err != nil {
@@ -139,8 +181,9 @@ func LoadState(filename string) (*Grid, error) {
} }
if explen != length { if explen != length {
return nil, fmt.Errorf(fmt.Sprintf("all rows must be in the same length, got: %d, expected: %d", return nil, fmt.Errorf(
length, explen)) fmt.Sprintf("all rows must be in the same length, got: %d, expected: %d",
length, explen))
} }
rows++ rows++
@@ -151,3 +194,14 @@ func LoadState(filename string) (*Grid, error) {
return grid, nil return grid, nil
} }
// generate filenames for dumps
func GetFilename(generations int64) string {
now := time.Now()
return fmt.Sprintf("dump-%s-%d.gol", now.Format("20060102150405"), generations)
}
func GetFilenameRLE(generations int64) string {
now := time.Now()
return fmt.Sprintf("rect-%s-%d.rle", now.Format("20060102150405"), generations)
}

113
loader-fonts.go Normal file
View File

@@ -0,0 +1,113 @@
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"
GameFontETXT string = "Noto Sans"
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 {
// load the font for use with ebitenui
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,
})
// load the font for use with etxt
fontlib := etxt.NewFontLibrary()
_, _, err = fontlib.ParseEmbedDirFonts(dir, assetfs)
if err != nil {
log.Fatalf("Error while loading fonts: %s", err.Error())
}
/*
err = fontlib.EachFont(
func(fontName string, font *etxt.Font) error {
fmt.Printf("font: %s\n", fontName)
return nil
})
if err != nil {
log.Fatal(err)
}
*/
if !fontlib.HasFont(GameFontETXT) {
log.Fatal("missing font: " + GameFontETXT)
}
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(GameFontETXT))
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
}

49
loader-shaders.go Normal file
View File

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

69
loader-sprites.go Normal file
View File

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

43
main.go
View File

@@ -4,32 +4,49 @@ import (
"fmt" "fmt"
"log" "log"
"os" "os"
"runtime/pprof"
_ "net/http/pprof"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
) )
func main() { func main() {
config := ParseCommandline() var directstart bool
if len(os.Args) > 1 {
directstart = true
}
config, err := ParseCommandline()
if err != nil {
log.Fatal(err)
}
if config.ShowVersion { if config.ShowVersion {
fmt.Printf("This is golsky version %s\n", VERSION) fmt.Printf("This is golsky version %s\n", VERSION)
os.Exit(0) os.Exit(0)
} }
// grid := [][]int64{ start := Play
// {0, 1, 1}, if !directstart {
// {0, 1, 0}, start = Menu
// {1, 1, 0}, config.DelayedStart = true
// } }
game := NewGame(config, SceneName(start))
// err := rle.StoreGridToRLE(grid, "test.rle", "B3/S23", 3, 3) if config.ProfileFile != "" {
// if err != nil { // enable cpu profiling. Do NOT use q to stop the game but
// panic(err) // close the window to get a profile
// } fd, err := os.Create(config.ProfileFile)
if err != nil {
log.Fatal(err)
}
defer fd.Close()
// os.Exit(0) pprof.StartCPUProfile(fd)
defer pprof.StopCPUProfile()
game := NewGame(config, Play) }
// main loop // main loop
if err := ebiten.RunGame(game); err != nil { if err := ebiten.RunGame(game); err != nil {

View File

@@ -20,6 +20,25 @@ type RLE struct {
patternLineIndex int patternLineIndex int
} }
// wrapper to load a RLE file
func GetRLE(filename string) (*RLE, error) {
if filename == "" {
return nil, nil
}
content, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
parsedRle, err := Parse(string(content))
if err != nil {
return nil, fmt.Errorf("failed to load RLE pattern file: %s", err)
}
return &parsedRle, nil
}
func Parse(input string) (RLE, error) { func Parse(input string) (RLE, error) {
rle := RLE{ rle := RLE{
inputLines: strings.Split(input, "\n"), inputLines: strings.Split(input, "\n"),

130
scene-menu.go Normal file
View File

@@ -0,0 +1,130 @@
package main
import (
"image/color"
"os"
"github.com/ebitenui/ebitenui"
"github.com/ebitenui/ebitenui/widget"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
)
type SceneMenu struct {
Game *Game
Config *Config
Next SceneName
Whoami SceneName
Ui *ebitenui.UI
FontColor color.RGBA
First bool
}
func NewMenuScene(game *Game, config *Config) Scene {
scene := &SceneMenu{
Whoami: Menu,
Game: game,
Next: Menu,
Config: config,
FontColor: color.RGBA{255, 30, 30, 0xff},
}
scene.Init()
return scene
}
func (scene *SceneMenu) GetNext() SceneName {
return scene.Next
}
func (scene *SceneMenu) ResetNext() {
scene.Next = scene.Whoami
}
func (scene *SceneMenu) SetNext(next SceneName) {
scene.Next = next
}
func (scene *SceneMenu) Update() error {
scene.Ui.Update()
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) || inpututil.IsKeyJustPressed(ebiten.KeyQ) {
scene.Config.DelayedStart = false
scene.Leave()
}
return nil
}
func (scene *SceneMenu) IsPrimary() bool {
return false
}
func (scene *SceneMenu) Draw(screen *ebiten.Image) {
scene.Ui.Draw(screen)
}
func (scene *SceneMenu) Leave() {
scene.SetNext(Play)
}
func (scene *SceneMenu) Init() {
rowContainer := NewRowContainer("Main Menu")
empty := NewMenuButton("Start with empty grid",
func(args *widget.ButtonClickedEventArgs) {
scene.Config.Empty = true
scene.Config.Restart = true
scene.Leave()
})
random := NewMenuButton("Start with random patterns",
func(args *widget.ButtonClickedEventArgs) {
scene.Config.Empty = false
scene.Config.Restart = true
scene.Leave()
})
copy := NewMenuButton("Save Copy as RLE",
func(args *widget.ButtonClickedEventArgs) {
scene.Config.Markmode = true
scene.Config.Paused = true
scene.Leave()
})
options := NewMenuButton("Options",
func(args *widget.ButtonClickedEventArgs) {
scene.SetNext(Options)
})
separator1 := NewSeparator()
separator2 := NewSeparator()
separator3 := NewSeparator()
cancel := NewMenuButton("Back",
func(args *widget.ButtonClickedEventArgs) {
scene.Leave()
})
quit := NewMenuButton("Exit Golsky",
func(args *widget.ButtonClickedEventArgs) {
os.Exit(0)
})
rowContainer.AddChild(empty)
rowContainer.AddChild(random)
rowContainer.AddChild(separator1)
rowContainer.AddChild(options)
rowContainer.AddChild(copy)
rowContainer.AddChild(separator2)
rowContainer.AddChild(cancel)
rowContainer.AddChild(separator3)
rowContainer.AddChild(quit)
scene.Ui = &ebitenui.UI{
Container: rowContainer.Container(),
}
}

121
scene-options.go Normal file
View File

@@ -0,0 +1,121 @@
package main
import (
"image/color"
"github.com/ebitenui/ebitenui"
"github.com/ebitenui/ebitenui/widget"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
)
type SceneOptions struct {
Game *Game
Config *Config
Next SceneName
Whoami SceneName
Ui *ebitenui.UI
FontColor color.RGBA
}
func NewOptionsScene(game *Game, config *Config) Scene {
scene := &SceneOptions{
Whoami: Options,
Game: game,
Next: Options,
Config: config,
FontColor: color.RGBA{255, 30, 30, 0xff},
}
scene.Init()
return scene
}
func (scene *SceneOptions) GetNext() SceneName {
return scene.Next
}
func (scene *SceneOptions) ResetNext() {
scene.Next = scene.Whoami
}
func (scene *SceneOptions) SetNext(next SceneName) {
scene.Next = next
}
func (scene *SceneOptions) IsPrimary() bool {
return false
}
func (scene *SceneOptions) Update() error {
scene.Ui.Update()
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) || inpututil.IsKeyJustPressed(ebiten.KeyQ) {
scene.SetNext(Play)
}
return nil
}
func (scene *SceneOptions) Draw(screen *ebiten.Image) {
scene.Ui.Draw(screen)
}
func (scene *SceneOptions) SetInitialValue(w *widget.LabeledCheckbox, value bool) {
var intval int
if value {
intval = 1
}
w.SetState(
widget.WidgetState(intval),
)
}
func (scene *SceneOptions) Init() {
rowContainer := NewRowContainer("Options")
pause := NewCheckbox("Pause",
func(args *widget.CheckboxChangedEventArgs) {
scene.Config.TogglePaused()
})
debugging := NewCheckbox("Debugging",
func(args *widget.CheckboxChangedEventArgs) {
scene.Config.ToggleDebugging()
})
scene.SetInitialValue(debugging, scene.Config.Debug)
invert := NewCheckbox("Invert",
func(args *widget.CheckboxChangedEventArgs) {
scene.Config.Invert = true
})
scene.SetInitialValue(invert, scene.Config.Invert)
gridlines := NewCheckbox("Show grid lines",
func(args *widget.CheckboxChangedEventArgs) {
scene.Config.ToggleGridlines()
})
scene.SetInitialValue(gridlines, scene.Config.ShowGrid)
separator := NewSeparator()
cancel := NewMenuButton("Close",
func(args *widget.ButtonClickedEventArgs) {
scene.SetNext(Menu)
})
rowContainer.AddChild(pause)
rowContainer.AddChild(debugging)
rowContainer.AddChild(invert)
rowContainer.AddChild(gridlines)
rowContainer.AddChild(separator)
rowContainer.AddChild(cancel)
scene.Ui = &ebitenui.UI{
Container: rowContainer.Container(),
}
}

View File

@@ -5,11 +5,9 @@ import (
"image" "image"
"image/color" "image/color"
"log" "log"
"math/rand"
"os" "os"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
"github.com/hajimehoshi/ebiten/v2/inpututil" "github.com/hajimehoshi/ebiten/v2/inpututil"
"github.com/hajimehoshi/ebiten/v2/vector" "github.com/hajimehoshi/ebiten/v2/vector"
"github.com/tlinden/golsky/rle" "github.com/tlinden/golsky/rle"
@@ -20,14 +18,20 @@ type Images struct {
Black, White, Age1, Age2, Age3, Age4, Old *ebiten.Image Black, White, Age1, Age2, Age3, Age4, Old *ebiten.Image
} }
const (
DEBUG_FORMAT = "FPS: %0.2f, TPG: %d, M: %0.2fMB, Generations: %d\nScale: %.02f, Zoom: %d, Cam: %.02f,%.02f Cursor: %d,%d %s"
)
type ScenePlay struct { type ScenePlay struct {
Game *Game Game *Game
Config *Config Config *Config
Next SceneName Next SceneName
Whoami SceneName Whoami SceneName
Clear bool
Grids []*Grid // 2 grids: one current, one next Grids []*Grid // 2 grids: one current, one next
History *Grid // holds state of past dead cells for evolution tracks History *Grid // holds state of past dead cells for evolution traces
Index int // points to current grid Index int // points to current grid
Generations int64 // Stats Generations int64 // Stats
Black, White, Grey, Old color.RGBA Black, White, Grey, Old color.RGBA
@@ -35,15 +39,14 @@ type ScenePlay struct {
TicksElapsed int // tick counter for game speed TicksElapsed int // tick counter for game speed
Tiles Images // pre-computed tiles for dead and alife cells Tiles Images // pre-computed tiles for dead and alife cells
Camera Camera // for zoom+move Camera Camera // for zoom+move
World *ebiten.Image // actual image we render to World, Cache *ebiten.Image // actual image we render to
WheelTurned bool // when user turns wheel multiple times, zoom faster WheelTurned bool // when user turns wheel multiple times, zoom faster
Dragging bool // middle mouse is pressed, move canvas Dragging bool // middle mouse is pressed, move canvas
LastCursorPos []int // used to check if the user is dragging LastCursorPos []int // used to check if the user is dragging
Markmode bool // enabled with 'c'
MarkTaken bool // true when mouse1 pressed MarkTaken bool // true when mouse1 pressed
MarkDone bool // true when mouse1 released, copy cells between Mark+Point MarkDone bool // true when mouse1 released, copy cells between Mark+Point
Mark, Point image.Point // area to marks+save Mark, Point image.Point // area to marks+save
Paused, RunOneStep bool // mutable flags from config RunOneStep bool // mutable flags from config
TPG int TPG int
} }
@@ -53,7 +56,6 @@ func NewPlayScene(game *Game, config *Config) Scene {
Game: game, Game: game,
Next: Play, Next: Play,
Config: config, Config: config,
Paused: config.Paused,
TPG: config.TPG, TPG: config.TPG,
RunOneStep: config.RunOneStep, RunOneStep: config.RunOneStep,
} }
@@ -63,6 +65,10 @@ func NewPlayScene(game *Game, config *Config) Scene {
return scene return scene
} }
func (scene *ScenePlay) IsPrimary() bool {
return true
}
func (scene *ScenePlay) GetNext() SceneName { func (scene *ScenePlay) GetNext() SceneName {
return scene.Next return scene.Next
} }
@@ -75,10 +81,6 @@ func (scene *ScenePlay) SetNext(next SceneName) {
scene.Next = next scene.Next = next
} }
func (scene *ScenePlay) Clearscreen() bool {
return true
}
func (scene *ScenePlay) CheckRule(state int64, neighbors int64) int64 { func (scene *ScenePlay) CheckRule(state int64, neighbors int64) int64 {
var nextstate int64 var nextstate int64
@@ -126,7 +128,7 @@ func (scene *ScenePlay) UpdateCells() {
// set history to current generation so we can infer the // set history to current generation so we can infer the
// age of the cell's state during rendering and use it to // age of the cell's state during rendering and use it to
// deduce the color to use if evolution tracking is enabled // deduce the color to use if evolution tracing is enabled
if state != nextstate { if state != nextstate {
scene.History.Data[y][x] = scene.Generations scene.History.Data[y][x] = scene.Generations
} }
@@ -149,9 +151,9 @@ func (scene *ScenePlay) UpdateCells() {
} }
func (scene *ScenePlay) Reset() { func (scene *ScenePlay) Reset() {
scene.Paused = true scene.Config.Paused = true
scene.InitGrid(nil) scene.InitGrid(nil)
scene.Paused = false scene.Config.Paused = false
} }
// check user input // check user input
@@ -160,38 +162,42 @@ func (scene *ScenePlay) CheckInput() {
os.Exit(0) os.Exit(0)
} }
if inpututil.IsKeyJustPressed(ebiten.KeyC) { if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
fmt.Println("mark mode on") scene.SetNext(Menu)
scene.Markmode = true
scene.Paused = true
} }
if scene.Markmode { if inpututil.IsKeyJustPressed(ebiten.KeyC) {
fmt.Println("mark mode on")
scene.Config.Markmode = true
scene.Config.Paused = true
}
if scene.Config.Markmode {
return return
} }
if inpututil.IsKeyJustPressed(ebiten.KeySpace) || inpututil.IsKeyJustPressed(ebiten.KeyEnter) { if inpututil.IsKeyJustPressed(ebiten.KeySpace) || inpututil.IsKeyJustPressed(ebiten.KeyEnter) {
scene.Paused = !scene.Paused scene.Config.TogglePaused()
} }
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
scene.ToggleCellOnCursorPos(Alive) scene.ToggleCellOnCursorPos(Alive)
scene.Paused = true // drawing while running makes no sense scene.Config.Paused = true // drawing while running makes no sense
} }
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight) { if ebiten.IsMouseButtonPressed(ebiten.MouseButtonRight) {
scene.ToggleCellOnCursorPos(Dead) scene.ToggleCellOnCursorPos(Dead)
scene.Paused = true // drawing while running makes no sense scene.Config.Paused = true // drawing while running makes no sense
} }
if ebiten.IsKeyPressed(ebiten.KeyPageDown) { if inpututil.IsKeyJustPressed(ebiten.KeyPageDown) {
if scene.Config.TPG < 120 { if scene.TPG < 120 {
scene.Config.TPG++ scene.TPG++
} }
} }
if ebiten.IsKeyPressed(ebiten.KeyPageUp) { if inpututil.IsKeyJustPressed(ebiten.KeyPageUp) {
if scene.TPG > 1 { if scene.TPG >= 1 {
scene.TPG-- scene.TPG--
} }
} }
@@ -200,11 +206,11 @@ func (scene *ScenePlay) CheckInput() {
scene.SaveState() scene.SaveState()
} }
if inpututil.IsKeyJustPressed(ebiten.KeyR) { if inpututil.IsKeyJustPressed(ebiten.KeyD) {
scene.Reset() scene.Config.Debug = !scene.Config.Debug
} }
if scene.Paused { if scene.Config.Paused {
if inpututil.IsKeyJustPressed(ebiten.KeyN) { if inpututil.IsKeyJustPressed(ebiten.KeyN) {
scene.Config.RunOneStep = true scene.Config.RunOneStep = true
} }
@@ -214,7 +220,7 @@ func (scene *ScenePlay) CheckInput() {
// Check dragging input. move the canvas with the mouse while pressing // Check dragging input. move the canvas with the mouse while pressing
// the middle mouse button, zoom in and out using the wheel. // the middle mouse button, zoom in and out using the wheel.
func (scene *ScenePlay) CheckDraggingInput() { func (scene *ScenePlay) CheckDraggingInput() {
if scene.Markmode { if scene.Config.Markmode {
return return
} }
@@ -258,28 +264,12 @@ func (scene *ScenePlay) CheckDraggingInput() {
// Zoom // Zoom
_, dy := ebiten.Wheel() _, dy := ebiten.Wheel()
step := 1
if scene.WheelTurned { if dy != 0 {
// if keep scrolling the wheel, zoom faster scene.Camera.ZoomFactor += (int(dy) * 5)
step = 50
} else {
scene.WheelTurned = false
} }
if dy < 0 { if inpututil.IsKeyJustPressed(ebiten.KeyR) {
if scene.Camera.ZoomFactor > -2400 {
scene.Camera.ZoomFactor -= step
}
}
if dy > 0 {
if scene.Camera.ZoomFactor < 2400 {
scene.Camera.ZoomFactor += step
}
}
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
scene.Camera.Reset() scene.Camera.Reset()
} }
@@ -294,7 +284,7 @@ func (scene *ScenePlay) GetWorldCursorPos() image.Point {
} }
func (scene *ScenePlay) CheckMarkInput() { func (scene *ScenePlay) CheckMarkInput() {
if !scene.Markmode { if !scene.Config.Markmode {
return return
} }
@@ -308,7 +298,7 @@ func (scene *ScenePlay) CheckMarkInput() {
scene.Point = scene.GetWorldCursorPos() scene.Point = scene.GetWorldCursorPos()
//fmt.Printf("Mark: %v, Point: %v\n", scene.Mark, scene.Point) //fmt.Printf("Mark: %v, Point: %v\n", scene.Mark, scene.Point)
} else if inpututil.IsMouseButtonJustReleased(ebiten.MouseButton0) { } else if inpututil.IsMouseButtonJustReleased(ebiten.MouseButton0) {
scene.Markmode = false scene.Config.Markmode = false
scene.MarkTaken = false scene.MarkTaken = false
scene.MarkDone = true scene.MarkDone = true
@@ -378,11 +368,25 @@ func (scene *ScenePlay) SaveRectRLE() {
} }
func (scene *ScenePlay) Update() error { func (scene *ScenePlay) Update() error {
if scene.Config.Restart {
scene.Config.Restart = false
scene.InitGrid(nil)
scene.InitCache()
return nil
}
if scene.Config.RestartCache {
scene.Config.RestartCache = false
scene.InitTiles()
scene.InitCache()
return nil
}
scene.CheckInput() scene.CheckInput()
scene.CheckDraggingInput() scene.CheckDraggingInput()
scene.CheckMarkInput() scene.CheckMarkInput()
if !scene.Paused || scene.RunOneStep { if !scene.Config.Paused || scene.RunOneStep {
scene.UpdateCells() scene.UpdateCells()
} }
@@ -396,9 +400,7 @@ func (scene *ScenePlay) ToggleCellOnCursorPos(alive int64) {
x := int(worldX) / scene.Config.Cellsize x := int(worldX) / scene.Config.Cellsize
y := int(worldY) / scene.Config.Cellsize y := int(worldY) / scene.Config.Cellsize
//fmt.Printf("cell at %d,%d\n", x, y) if x > -1 && y > -1 && x < scene.Config.Width && y < scene.Config.Height {
if x > -1 && y > -1 {
scene.Grids[scene.Index].Data[y][x] = alive scene.Grids[scene.Index].Data[y][x] = alive
scene.History.Data[y][x] = 1 scene.History.Data[y][x] = 1
} }
@@ -411,27 +413,31 @@ func (scene *ScenePlay) Draw(screen *ebiten.Image) {
// a nice grey grid with grid lines // a nice grey grid with grid lines
op := &ebiten.DrawImageOptions{} op := &ebiten.DrawImageOptions{}
if scene.Config.NoGrid { op.GeoM.Translate(0, 0)
scene.World.Fill(scene.White) scene.World.DrawImage(scene.Cache, op)
} else {
scene.World.Fill(scene.Grey) var age int64
}
for y := 0; y < scene.Config.Height; y++ { for y := 0; y < scene.Config.Height; y++ {
for x := 0; x < scene.Config.Width; x++ { for x := 0; x < scene.Config.Width; x++ {
op.GeoM.Reset() op.GeoM.Reset()
op.GeoM.Translate(float64(x*scene.Config.Cellsize), float64(y*scene.Config.Cellsize)) op.GeoM.Translate(
float64(x*scene.Config.Cellsize),
float64(y*scene.Config.Cellsize),
)
age := scene.Generations - scene.History.Data[y][x] age = scene.Generations - scene.History.Data[y][x]
switch scene.Grids[scene.Index].Data[y][x] { switch scene.Grids[scene.Index].Data[y][x] {
case 1: case Alive:
if age > 50 && scene.Config.ShowEvolution { if age > 50 && scene.Config.ShowEvolution {
scene.World.DrawImage(scene.Tiles.Old, op) scene.World.DrawImage(scene.Tiles.Old, op)
} else { } else {
scene.World.DrawImage(scene.Tiles.Black, op) scene.World.DrawImage(scene.Tiles.Black, op)
} }
case 0: case Dead:
// only draw dead cells in case evolution trace is enabled
if scene.History.Data[y][x] > 1 && scene.Config.ShowEvolution { if scene.History.Data[y][x] > 1 && scene.Config.ShowEvolution {
switch { switch {
case age < 10: case age < 10:
@@ -443,22 +449,30 @@ func (scene *ScenePlay) Draw(screen *ebiten.Image) {
default: default:
scene.World.DrawImage(scene.Tiles.Age4, op) scene.World.DrawImage(scene.Tiles.Age4, op)
} }
} else {
scene.World.DrawImage(scene.Tiles.White, op)
} }
} }
} }
} }
if scene.Markmode && scene.MarkTaken { scene.DrawMark(scene.World)
scene.Camera.Render(scene.World, screen)
scene.DrawDebug(screen)
op.GeoM.Reset()
op.GeoM.Translate(0, 0)
scene.Game.Screen.DrawImage(screen, op)
}
func (scene *ScenePlay) DrawMark(screen *ebiten.Image) {
if scene.Config.Markmode && scene.MarkTaken {
x := float32(scene.Mark.X * scene.Config.Cellsize) x := float32(scene.Mark.X * scene.Config.Cellsize)
y := float32(scene.Mark.Y * scene.Config.Cellsize) y := float32(scene.Mark.Y * scene.Config.Cellsize)
w := float32((scene.Point.X - scene.Mark.X) * scene.Config.Cellsize) w := float32((scene.Point.X - scene.Mark.X) * scene.Config.Cellsize)
h := float32((scene.Point.Y - scene.Mark.Y) * scene.Config.Cellsize) h := float32((scene.Point.Y - scene.Mark.Y) * scene.Config.Cellsize)
// fmt.Printf("%d,%d=>%0.0f,%0.0f to %d,%d=>%0.0f,%0.0f\n",
// scene.Mark.X, scene.Mark.Y, x, y, scene.Point.X, scene.Point.Y, w, h)
vector.StrokeRect( vector.StrokeRect(
scene.World, scene.World,
x+1, y+1, x+1, y+1,
@@ -466,45 +480,68 @@ func (scene *ScenePlay) Draw(screen *ebiten.Image) {
1.0, scene.Old, false, 1.0, scene.Old, false,
) )
} }
}
scene.Camera.Render(scene.World, screen) func (scene *ScenePlay) DrawDebug(screen *ebiten.Image) {
if scene.Config.Debug { if scene.Config.Debug {
paused := "" paused := ""
if scene.Paused { if scene.Config.Paused {
paused = "-- paused --" paused = "-- paused --"
} }
ebitenutil.DebugPrint( x, y := ebiten.CursorPosition()
screen, debug := fmt.Sprintf(
fmt.Sprintf("FPS: %0.2f, TPG: %d, Mem: %0.2f MB, Generations: %d %s", DEBUG_FORMAT,
ebiten.ActualTPS(), scene.TPG, GetMem(), scene.Generations, paused), ebiten.ActualTPS(), scene.TPG, GetMem(), scene.Generations,
) scene.Game.Scale, scene.Camera.ZoomFactor,
scene.Camera.Position[0], scene.Camera.Position[1],
x, y,
paused)
FontRenderer.Renderer.SetSizePx(10 + int(scene.Game.Scale*10))
FontRenderer.Renderer.SetTarget(screen)
FontRenderer.Renderer.SetColor(scene.Black)
FontRenderer.Renderer.Draw(debug, 31, 31)
FontRenderer.Renderer.SetColor(scene.Old)
FontRenderer.Renderer.Draw(debug, 30, 30)
fmt.Println(debug)
} }
} }
// FIXME: move these into Grid
// load a pre-computed pattern from RLE file // load a pre-computed pattern from RLE file
func (scene *ScenePlay) InitPattern() { func (scene *ScenePlay) InitPattern() {
if scene.Config.RLE != nil { scene.Grids[0].LoadRLE(scene.Config.RLE)
startX := (scene.Config.Width / 2) - (scene.Config.RLE.Width / 2) scene.History.LoadRLE(scene.Config.RLE)
startY := (scene.Config.Height / 2) - (scene.Config.RLE.Height / 2) }
var y, x int
for rowIndex, patternRow := range scene.Config.RLE.Pattern { // pre-render offscreen cache image
for colIndex := range patternRow { func (scene *ScenePlay) InitCache() {
if scene.Config.RLE.Pattern[rowIndex][colIndex] > 0 { op := &ebiten.DrawImageOptions{}
x = colIndex + startX
y = rowIndex + startY
scene.History.Data[y][x] = 1 if scene.Config.ShowGrid {
scene.Grids[0].Data[y][x] = 1 scene.Cache.Fill(scene.Grey)
} } else {
} scene.Cache.Fill(scene.White)
}
for y := 0; y < scene.Config.Height; y++ {
for x := 0; x < scene.Config.Width; x++ {
op.GeoM.Reset()
op.GeoM.Translate(
float64(x*scene.Config.Cellsize),
float64(y*scene.Config.Cellsize),
)
scene.Cache.DrawImage(scene.Tiles.White, op)
} }
} }
} }
// initialize grid[s], either using pre-computed from state or rle file, or random
func (scene *ScenePlay) InitGrid(grid *Grid) { func (scene *ScenePlay) InitGrid(grid *Grid) {
if grid != nil { if grid != nil {
// use pre-loaded grid // use pre-loaded grid
@@ -522,16 +559,9 @@ func (scene *ScenePlay) InitGrid(grid *Grid) {
gridb := NewGrid(scene.Config.Width, scene.Config.Height, scene.Config.Density, scene.Config.Empty) gridb := NewGrid(scene.Config.Width, scene.Config.Height, scene.Config.Density, scene.Config.Empty)
history := NewGrid(scene.Config.Width, scene.Config.Height, scene.Config.Density, scene.Config.Empty) history := NewGrid(scene.Config.Width, scene.Config.Height, scene.Config.Density, scene.Config.Empty)
for y := 0; y < scene.Config.Height; y++ { // startup is delayed until user has selected options
if !scene.Config.Empty { grida.FillRandom()
for x := 0; x < scene.Config.Width; x++ { grida.Copy(history)
if rand.Intn(scene.Config.Density) == 1 {
history.Data[y][x] = 1
grida.Data[y][x] = 1
}
}
}
}
scene.Grids = []*Grid{ scene.Grids = []*Grid{
grida, grida,
@@ -588,7 +618,6 @@ func (scene *ScenePlay) Init() {
if scene.Config.StateGrid != nil { if scene.Config.StateGrid != nil {
grid = scene.Config.StateGrid grid = scene.Config.StateGrid
} }
scene.Camera = Camera{ scene.Camera = Camera{
@@ -598,16 +627,40 @@ func (scene *ScenePlay) Init() {
}, },
} }
scene.World = ebiten.NewImage(scene.Config.ScreenWidth, scene.Config.ScreenHeight) scene.World = ebiten.NewImage(
scene.Config.Width*scene.Config.Cellsize,
scene.Config.Height*scene.Config.Cellsize,
)
scene.Cache = ebiten.NewImage(
scene.Config.Width*scene.Config.Cellsize,
scene.Config.Height*scene.Config.Cellsize,
)
scene.InitGrid(grid)
scene.InitPattern()
scene.InitTiles() scene.InitTiles()
scene.InitCache()
if scene.Config.DelayedStart && !scene.Config.Empty {
scene.Config.Empty = true
scene.InitGrid(grid)
scene.Config.Empty = false
} else {
scene.InitGrid(grid)
}
scene.InitPattern()
scene.Index = 0 scene.Index = 0
scene.TicksElapsed = 0 scene.TicksElapsed = 0
scene.LastCursorPos = make([]int, 2) scene.LastCursorPos = make([]int, 2)
if scene.Config.Zoomfactor < 0 || scene.Config.Zoomfactor > 0 {
scene.Camera.ZoomFactor = scene.Config.Zoomfactor
}
scene.Camera.Position[0] = scene.Config.InitialCamPos[0]
scene.Camera.Position[1] = scene.Config.InitialCamPos[1]
} }
// count the living neighbors of a cell // count the living neighbors of a cell

View File

@@ -14,12 +14,13 @@ type Scene interface {
SetNext(SceneName) SetNext(SceneName)
GetNext() SceneName GetNext() SceneName
ResetNext() ResetNext()
Clearscreen() bool
Update() error Update() error
Draw(screen *ebiten.Image) Draw(screen *ebiten.Image)
IsPrimary() bool // if true, this scene will be always drawn
} }
const ( const (
Menu = iota // main top level menu Menu = iota // main top level menu
Play // actual playing happens here Play // actual playing happens here
Options
) )

161
widgets.go Normal file
View File

@@ -0,0 +1,161 @@
package main
import (
"image/color"
"github.com/ebitenui/ebitenui/image"
"github.com/ebitenui/ebitenui/widget"
)
func NewMenuButton(
text string,
action func(args *widget.ButtonClickedEventArgs)) *widget.Button {
buttonImage, _ := LoadButtonImage()
return widget.NewButton(
widget.ButtonOpts.WidgetOpts(
widget.WidgetOpts.LayoutData(widget.RowLayoutData{
Position: widget.RowLayoutPositionCenter,
Stretch: true,
MaxWidth: 200,
MaxHeight: 100,
}),
),
widget.ButtonOpts.Image(buttonImage),
widget.ButtonOpts.Text(text, *FontRenderer.FontSmall, &widget.ButtonTextColor{
Idle: color.NRGBA{0xdf, 0xf4, 0xff, 0xff},
}),
widget.ButtonOpts.TextPadding(widget.Insets{
Left: 5,
Right: 5,
Top: 5,
Bottom: 5,
}),
widget.ButtonOpts.ClickedHandler(action),
)
}
func NewCheckbox(
text string,
action func(args *widget.CheckboxChangedEventArgs)) *widget.LabeledCheckbox {
checkboxImage, _ := LoadCheckboxImage()
buttonImage, _ := LoadButtonImage()
return widget.NewLabeledCheckbox(
widget.LabeledCheckboxOpts.CheckboxOpts(
widget.CheckboxOpts.ButtonOpts(
widget.ButtonOpts.Image(buttonImage),
),
widget.CheckboxOpts.Image(checkboxImage),
widget.CheckboxOpts.StateChangedHandler(action),
),
widget.LabeledCheckboxOpts.LabelOpts(
widget.LabelOpts.Text(text, *FontRenderer.FontSmall,
&widget.LabelColor{
Idle: color.NRGBA{0xdf, 0xf4, 0xff, 0xff},
}),
),
)
}
func NewSeparator() widget.PreferredSizeLocateableWidget {
c := widget.NewContainer(
widget.ContainerOpts.Layout(widget.NewRowLayout(
widget.RowLayoutOpts.Direction(widget.DirectionVertical),
widget.RowLayoutOpts.Padding(widget.Insets{
Top: 3,
Bottom: 0,
}))),
widget.ContainerOpts.WidgetOpts(
widget.WidgetOpts.LayoutData(
widget.RowLayoutData{Stretch: true})))
return c
}
type RowContainer struct {
Root *widget.Container
Row *widget.Container
}
func (container *RowContainer) AddChild(child widget.PreferredSizeLocateableWidget) {
container.Row.AddChild(child)
}
func (container *RowContainer) Container() *widget.Container {
return container.Root
}
// set arg to false if no background needed
func NewRowContainer(title string) *RowContainer {
buttonImageHover := image.NewNineSlice(Assets["button-9slice3"], [3]int{3, 3, 3}, [3]int{3, 3, 3})
uiContainer := widget.NewContainer(
widget.ContainerOpts.Layout(widget.NewAnchorLayout()),
)
titleLabel := widget.NewText(
widget.TextOpts.WidgetOpts(widget.WidgetOpts.LayoutData(widget.RowLayoutData{
Stretch: true,
})),
widget.TextOpts.Text(title, *FontRenderer.FontNormal, color.NRGBA{0xdf, 0xf4, 0xff, 0xff}))
rowContainer := widget.NewContainer(
widget.ContainerOpts.WidgetOpts(
widget.WidgetOpts.LayoutData(widget.AnchorLayoutData{
HorizontalPosition: widget.AnchorLayoutPositionCenter,
VerticalPosition: widget.AnchorLayoutPositionCenter,
}),
),
widget.ContainerOpts.Layout(widget.NewRowLayout(
widget.RowLayoutOpts.Direction(widget.DirectionVertical),
widget.RowLayoutOpts.Padding(widget.NewInsetsSimple(8)),
widget.RowLayoutOpts.Spacing(0),
)),
widget.ContainerOpts.BackgroundImage(buttonImageHover),
)
rowContainer.AddChild(titleLabel)
uiContainer.AddChild(rowContainer)
return &RowContainer{
Root: uiContainer,
Row: rowContainer,
}
}
func LoadButtonImage() (*widget.ButtonImage, error) {
idle := image.NewNineSlice(Assets["button-9slice2"], [3]int{3, 3, 3}, [3]int{3, 3, 3})
hover := image.NewNineSlice(Assets["button-9slice3"], [3]int{3, 3, 3}, [3]int{3, 3, 3})
pressed := image.NewNineSlice(Assets["button-9slice1"], [3]int{3, 3, 3}, [3]int{3, 3, 3})
return &widget.ButtonImage{
Idle: idle,
Hover: hover,
Pressed: pressed,
}, nil
}
func LoadCheckboxImage() (*widget.CheckboxGraphicImage, error) {
unchecked := &widget.ButtonImageImage{
Idle: Assets["checkbox-9slice2"],
Disabled: Assets["checkbox-9slice2"],
}
checked := &widget.ButtonImageImage{
Idle: Assets["checkbox-9slice1"],
Disabled: Assets["checkbox-9slice1"],
}
return &widget.CheckboxGraphicImage{
Checked: checked,
Unchecked: unchecked,
Greyed: unchecked,
}, nil
}