From 81c4b976e27935b7fc9c602a665d7cef648494f9 Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Fri, 7 Jun 2024 17:27:08 +0200 Subject: [PATCH] added theme system, makes it easier to add more color schemes --- play.go | 0 src/config.go | 27 +++++++++++ src/generics.go | 7 +++ src/play.go | 105 ++++++++++++----------------------------- src/theme.go | 123 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 188 insertions(+), 74 deletions(-) create mode 100644 play.go create mode 100644 src/theme.go diff --git a/play.go b/play.go new file mode 100644 index 0000000..e69de29 diff --git a/src/config.go b/src/config.go index 672f074..93dd0fd 100644 --- a/src/config.go +++ b/src/config.go @@ -33,6 +33,8 @@ type Config struct { ZoomOutFactor int InitialCamPos []float64 DelayedStart bool // if true game, we wait. like pause but program induced + Theme string + ThemeManager ThemeManager // for internal profiling ProfileFile string @@ -50,6 +52,7 @@ const ( DEFAULT_CELLSIZE = 4 DEFAULT_ZOOMFACTOR = 150 DEFAULT_GEOM = "640x384" + DEFAULT_THEME = "light" // inverse => "dark" ) const KEYBINDINGS string = ` @@ -224,7 +227,11 @@ func ParseCommandline() (*Config, error) { pflag.BoolVarP(&config.Debug, "debug", "d", false, "show debug info") pflag.BoolVarP(&config.ShowGrid, "show-grid", "g", false, "draw grid lines") pflag.BoolVarP(&config.Empty, "empty", "e", false, "start with an empty screen") + + // style pflag.BoolVarP(&config.Invert, "invert", "i", false, "invert colors (dead cell: black)") + pflag.StringVarP(&config.Theme, "theme", "T", "light", "color theme: dark, light (default: light)") + 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.UseShader, "use-shader", "k", false, "use shader for cell rendering") @@ -253,6 +260,12 @@ func ParseCommandline() (*Config, error) { config.SetupCamera() + if config.Theme == "light" && config.Invert { + config.Theme = "dark" + } + + config.ThemeManager = NewThemeManager(config.Theme, config.Cellsize) + //repr.Println(config) return &config, nil } @@ -267,9 +280,23 @@ func (config *Config) ToggleDebugging() { func (config *Config) ToggleInvert() { config.Invert = !config.Invert + + switch config.ThemeManager.GetCurrentThemeName() { + case "dark": + config.ThemeManager.SetCurrentTheme("light") + case "light": + config.ThemeManager.SetCurrentTheme("dark") + default: + fmt.Println("unable to invert custom theme, only possible with dark and light themes.") + } + config.RestartCache = true } +func (config *Config) SwitchTheme(theme string) { + config.ThemeManager.SetCurrentTheme(theme) +} + func (config *Config) ToggleGridlines() { config.ShowGrid = !config.ShowGrid config.RestartCache = true diff --git a/src/generics.go b/src/generics.go index e939ebe..c71e1da 100644 --- a/src/generics.go +++ b/src/generics.go @@ -10,3 +10,10 @@ func Contains[E comparable](s []E, v E) bool { return false } + +func Exists[K comparable, V any](m map[K]V, v K) bool { + if _, ok := m[v]; ok { + return true + } + return false +} diff --git a/src/play.go b/src/play.go index 422e933..d1b30d4 100644 --- a/src/play.go +++ b/src/play.go @@ -31,24 +31,22 @@ type ScenePlay struct { Clear bool - Grids []*Grid // 2 grids: one current, one next - History [][]int64 // holds state of past dead cells for evolution traces - Index int // points to current grid - Generations int64 // Stats - Black, White, Grey, Old color.RGBA - AgeColor1, AgeColor2, AgeColor3, AgeColor4 color.RGBA - TicksElapsed int // tick counter for game speed - Tiles Images // pre-computed tiles for dead and alife cells - Camera Camera // for zoom+move - World, Cache *ebiten.Image // actual image we render to - WheelTurned bool // when user turns wheel multiple times, zoom faster - Dragging bool // middle mouse is pressed, move canvas - LastCursorPos []float64 // used to check if the user is dragging - MarkTaken bool // true when mouse1 pressed - MarkDone bool // true when mouse1 released, copy cells between Mark+Point - Mark, Point image.Point // area to marks+save - RunOneStep bool // mutable flags from config - TPG int // current game speed (ticks per game) + Grids []*Grid // 2 grids: one current, one next + History [][]int64 // holds state of past dead cells for evolution traces + Index int // points to current grid + Generations int64 // Stats + TicksElapsed int // tick counter for game speed + Camera Camera // for zoom+move + World, Cache *ebiten.Image // actual image we render to + WheelTurned bool // when user turns wheel multiple times, zoom faster + Dragging bool // middle mouse is pressed, move canvas + LastCursorPos []float64 // used to check if the user is dragging + MarkTaken bool // true when mouse1 pressed + MarkDone bool // true when mouse1 released, copy cells between Mark+Point + Mark, Point image.Point // area to marks+save + RunOneStep bool // mutable flags from config + TPG int // current game speed (ticks per game) + Theme Theme } func NewPlayScene(game *Game, config *Config) Scene { @@ -417,7 +415,7 @@ func (scene *ScenePlay) Update() error { if scene.Config.RestartCache { scene.Config.RestartCache = false - scene.InitTiles() + scene.Theme = scene.Config.ThemeManager.GetCurrentTheme() scene.InitCache() return nil } @@ -473,7 +471,7 @@ func (scene *ScenePlay) Draw(screen *ebiten.Image) { scene.DrawEvolution(screen, x, y, op) } else { if scene.Grids[scene.Index].Data[y][x] { - scene.World.DrawImage(scene.Tiles.Black, op) + scene.World.DrawImage(scene.Theme.Tile(ColLife), op) } } } @@ -497,22 +495,22 @@ func (scene *ScenePlay) DrawEvolution(screen *ebiten.Image, x, y int, op *ebiten switch scene.Grids[scene.Index].Data[y][x] { case Alive: if age > 50 && scene.Config.ShowEvolution { - scene.World.DrawImage(scene.Tiles.Old, op) + scene.World.DrawImage(scene.Theme.Tile(ColOld), op) } else { - scene.World.DrawImage(scene.Tiles.Black, op) + scene.World.DrawImage(scene.Theme.Tile(ColLife), op) } case Dead: // only draw dead cells in case evolution trace is enabled if scene.History[y][x] > 1 && scene.Config.ShowEvolution { switch { case age < 10: - scene.World.DrawImage(scene.Tiles.Age1, op) + scene.World.DrawImage(scene.Theme.Tile(ColAge1), op) case age < 20: - scene.World.DrawImage(scene.Tiles.Age2, op) + scene.World.DrawImage(scene.Theme.Tile(ColAge2), op) case age < 30: - scene.World.DrawImage(scene.Tiles.Age3, op) + scene.World.DrawImage(scene.Theme.Tile(ColAge3), op) default: - scene.World.DrawImage(scene.Tiles.Age4, op) + scene.World.DrawImage(scene.Theme.Tile(ColAge4), op) } } } @@ -529,7 +527,7 @@ func (scene *ScenePlay) DrawMark(screen *ebiten.Image) { scene.World, x+1, y+1, w, h, - 1.0, scene.Old, false, + 1.0, scene.Theme.Color(ColOld), false, ) } } @@ -561,10 +559,10 @@ func (scene *ScenePlay) DrawDebug(screen *ebiten.Image) { FontRenderer.Renderer.SetSizePx(10 + int(scene.Game.Scale*10)) FontRenderer.Renderer.SetTarget(screen) - FontRenderer.Renderer.SetColor(scene.Black) + FontRenderer.Renderer.SetColor(scene.Theme.Color(ColLife)) FontRenderer.Renderer.Draw(debug, 31, 31) - FontRenderer.Renderer.SetColor(scene.Old) + FontRenderer.Renderer.SetColor(scene.Theme.Color(ColOld)) FontRenderer.Renderer.Draw(debug, 30, 30) fmt.Println(debug) @@ -582,9 +580,9 @@ func (scene *ScenePlay) InitCache() { op := &ebiten.DrawImageOptions{} if scene.Config.ShowGrid { - scene.Cache.Fill(scene.Grey) + scene.Cache.Fill(scene.Theme.Color(ColGrid)) } else { - scene.Cache.Fill(scene.White) + scene.Cache.Fill(scene.Theme.Color(ColDead)) } for y := 0; y < scene.Config.Height; y++ { @@ -595,7 +593,7 @@ func (scene *ScenePlay) InitCache() { float64(y*scene.Config.Cellsize), ) - scene.Cache.DrawImage(scene.Tiles.White, op) + scene.Cache.DrawImage(scene.Theme.Tile(ColDead), op) } } } @@ -619,47 +617,6 @@ func (scene *ScenePlay) InitGrid() { } } -// prepare tile images -func (scene *ScenePlay) InitTiles() { - scene.Grey = color.RGBA{128, 128, 128, 0xff} - scene.Old = color.RGBA{255, 30, 30, 0xff} - - scene.Black = color.RGBA{0, 0, 0, 0xff} - scene.White = color.RGBA{200, 200, 200, 0xff} - scene.AgeColor1 = color.RGBA{255, 195, 97, 0xff} // FIXME: use slice! - scene.AgeColor2 = color.RGBA{255, 211, 140, 0xff} - scene.AgeColor3 = color.RGBA{255, 227, 181, 0xff} - scene.AgeColor4 = color.RGBA{255, 240, 224, 0xff} - - if scene.Config.Invert { - scene.White = color.RGBA{0, 0, 0, 0xff} - scene.Black = color.RGBA{200, 200, 200, 0xff} - - scene.AgeColor1 = color.RGBA{82, 38, 0, 0xff} - scene.AgeColor2 = color.RGBA{66, 35, 0, 0xff} - scene.AgeColor3 = color.RGBA{43, 27, 0, 0xff} - scene.AgeColor4 = color.RGBA{25, 17, 0, 0xff} - } - - scene.Tiles.Black = ebiten.NewImage(scene.Config.Cellsize, scene.Config.Cellsize) - scene.Tiles.White = ebiten.NewImage(scene.Config.Cellsize, scene.Config.Cellsize) - scene.Tiles.Old = ebiten.NewImage(scene.Config.Cellsize, scene.Config.Cellsize) - scene.Tiles.Age1 = ebiten.NewImage(scene.Config.Cellsize, scene.Config.Cellsize) - scene.Tiles.Age2 = ebiten.NewImage(scene.Config.Cellsize, scene.Config.Cellsize) - scene.Tiles.Age3 = ebiten.NewImage(scene.Config.Cellsize, scene.Config.Cellsize) - scene.Tiles.Age4 = ebiten.NewImage(scene.Config.Cellsize, scene.Config.Cellsize) - - cellsize := scene.Config.ScreenWidth / scene.Config.Cellsize - - FillCell(scene.Tiles.Black, cellsize, scene.Black) - FillCell(scene.Tiles.White, cellsize, scene.White) - FillCell(scene.Tiles.Old, cellsize, scene.Old) - FillCell(scene.Tiles.Age1, cellsize, scene.AgeColor1) - FillCell(scene.Tiles.Age2, cellsize, scene.AgeColor2) - FillCell(scene.Tiles.Age3, cellsize, scene.AgeColor3) - FillCell(scene.Tiles.Age4, cellsize, scene.AgeColor4) -} - func (scene *ScenePlay) Init() { // setup the scene scene.Camera = Camera{ @@ -685,7 +642,7 @@ func (scene *ScenePlay) Init() { scene.Config.Height*scene.Config.Cellsize, ) - scene.InitTiles() + scene.Theme = scene.Config.ThemeManager.GetCurrentTheme() scene.InitCache() if scene.Config.DelayedStart && !scene.Config.Empty { diff --git a/src/theme.go b/src/theme.go new file mode 100644 index 0000000..4b5b74e --- /dev/null +++ b/src/theme.go @@ -0,0 +1,123 @@ +package main + +import ( + "image/color" + + "github.com/hajimehoshi/ebiten/v2" +) + +// Color definitions. ColLife could be black or white depending on theme +const ( + ColLife = iota + ColDead + ColOld + ColAge1 + ColAge2 + ColAge3 + ColAge4 + ColGrid +) + +// A Theme defines how the grid and the cells are colored. We define +// the colors and the actual tile images here, so that they are +// readily available from play.go +type Theme struct { + Tiles map[int]*ebiten.Image + Colors map[int]color.RGBA + Name string +} + +// create a new theme +func NewTheme(life, dead, old, age1, age2, age3, age4, grid color.RGBA, cellsize int, name string) Theme { + theme := Theme{ + Name: name, + Colors: map[int]color.RGBA{ + ColLife: life, + ColDead: dead, + ColGrid: grid, + ColAge1: age1, + ColAge2: age2, + ColAge3: age3, + ColAge4: age4, + ColOld: old, + }, + } + + theme.Tiles = make(map[int]*ebiten.Image, 6) + + for cid, col := range theme.Colors { + theme.Tiles[cid] = ebiten.NewImage(cellsize, cellsize) + FillCell(theme.Tiles[cid], cellsize, col) + } + + return theme +} + +// return the tile image for the requested color type. panic if +// unknown type is being used, which is ok, since the code is the only +// user anyway +func (theme *Theme) Tile(col int) *ebiten.Image { + return theme.Tiles[col] +} + +func (theme *Theme) Color(col int) color.RGBA { + return theme.Colors[col] +} + +type ThemeManager struct { + Theme string + Themes map[string]Theme +} + +// Manager is used to easily switch themes from cli or menu +func NewThemeManager(initial string, cellsize int) ThemeManager { + light := NewTheme( + color.RGBA{0, 0, 0, 0xff}, // life + color.RGBA{200, 200, 200, 0xff}, // dead + color.RGBA{255, 30, 30, 0xff}, // old + color.RGBA{255, 195, 97, 0xff}, // age 1..4 + color.RGBA{255, 211, 140, 0xff}, + color.RGBA{255, 227, 181, 0xff}, + color.RGBA{255, 240, 224, 0xff}, + color.RGBA{128, 128, 128, 0xff}, // grid + cellsize, + "light", + ) + + dark := NewTheme( + color.RGBA{200, 200, 200, 0xff}, // life + color.RGBA{0, 0, 0, 0xff}, // dead + color.RGBA{255, 30, 30, 0xff}, // old + color.RGBA{82, 38, 0, 0xff}, // age 1..4 + color.RGBA{66, 35, 0, 0xff}, + color.RGBA{43, 27, 0, 0xff}, + color.RGBA{25, 17, 0, 0xff}, + color.RGBA{128, 128, 128, 0xff}, // grid + cellsize, + "dark", + ) + + manager := ThemeManager{ + Themes: map[string]Theme{ + "dark": dark, + "light": light, + }, + Theme: initial, + } + + return manager +} + +func (manager *ThemeManager) GetCurrentTheme() Theme { + return manager.Themes[manager.Theme] +} + +func (manager *ThemeManager) GetCurrentThemeName() string { + return manager.Theme +} + +func (manager *ThemeManager) SetCurrentTheme(theme string) { + if Exists(manager.Themes, theme) { + manager.Theme = theme + } +}