diff --git a/camera.go b/camera.go new file mode 100644 index 0000000..a0cb42f --- /dev/null +++ b/camera.go @@ -0,0 +1,68 @@ +package main + +import ( + "fmt" + "math" + + "github.com/hajimehoshi/ebiten/v2" + "golang.org/x/image/math/f64" +) + +type Camera struct { + ViewPort f64.Vec2 + Position f64.Vec2 + ZoomFactor int +} + +func (c *Camera) String() string { + return fmt.Sprintf( + "T: %.1f, S: %d", + c.Position, c.ZoomFactor, + ) +} + +func (c *Camera) viewportCenter() f64.Vec2 { + return f64.Vec2{ + c.ViewPort[0] * 0.5, + c.ViewPort[1] * 0.5, + } +} + +func (c *Camera) worldMatrix() ebiten.GeoM { + m := ebiten.GeoM{} + m.Translate(-c.Position[0], -c.Position[1]) + + // We want to scale and rotate around center of image / screen + m.Translate(-c.viewportCenter()[0], -c.viewportCenter()[1]) + + m.Scale( + math.Pow(1.01, float64(c.ZoomFactor)), + math.Pow(1.01, float64(c.ZoomFactor)), + ) + + m.Translate(c.viewportCenter()[0], c.viewportCenter()[1]) + return m +} + +func (c *Camera) Render(world, screen *ebiten.Image) { + screen.DrawImage(world, &ebiten.DrawImageOptions{ + GeoM: c.worldMatrix(), + }) +} + +func (c *Camera) ScreenToWorld(posX, posY int) (float64, float64) { + inverseMatrix := c.worldMatrix() + if inverseMatrix.IsInvertible() { + inverseMatrix.Invert() + return inverseMatrix.Apply(float64(posX), float64(posY)) + } else { + // When scaling it can happened that matrix is not invertable + return math.NaN(), math.NaN() + } +} + +func (c *Camera) Reset() { + c.Position[0] = 0 + c.Position[1] = 0 + c.ZoomFactor = 0 +} diff --git a/go.mod b/go.mod index 3e83f82..e9aafdd 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,9 @@ module github.com/tlinden/gameoflife go 1.22 require ( - github.com/alecthomas/repr v0.4.0 github.com/hajimehoshi/ebiten/v2 v2.7.4 github.com/spf13/pflag v1.0.5 + golang.org/x/image v0.16.0 ) require ( diff --git a/go.sum b/go.sum index 0fa22ed..3b0eded 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= -github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895 h1:48bCqKTuD7Z0UovDfvpCn7wZ0GUZ+yosIteNDthn3FU= github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895/go.mod h1:XZdLv05c5hOZm3fM2NlJ92FyEZjnslcMcNRrhxs8+8M= github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE= diff --git a/main.go b/main.go index bd496d1..4dcad5b 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/tlinden/gameoflife/rle" + "golang.org/x/image/math/f64" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/ebitenutil" @@ -48,6 +49,9 @@ type Game struct { Rule *Rule // which rule to use, default: B3/S23 Tiles Images // pre-computed tiles for dead and alife cells RLE *rle.RLE // loaded GOL pattern from RLE file + Camera Camera + World *ebiten.Image + WheelTurned bool } func (game *Game) Layout(outsideWidth, outsideHeight int) (int, int) { @@ -195,38 +199,74 @@ func (game *Game) CheckInput() { game.Paused = true // drawing while running makes no sense } - if inpututil.IsKeyJustPressed(ebiten.KeyArrowDown) { + if ebiten.IsKeyPressed(ebiten.KeyPageDown) { if game.TPG < 120 { game.TPG++ } } - if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) { + if ebiten.IsKeyPressed(ebiten.KeyPageUp) { if game.TPG > 1 { game.TPG-- } } - if inpututil.IsKeyJustPressed(ebiten.KeyPageDown) { - if game.TPG <= 115 { - game.TPG += 5 - } - } - - if inpututil.IsKeyJustPressed(ebiten.KeyPageUp) { - switch { - case game.TPG > 5: - game.TPG -= 5 - case game.TPG <= 5: - game.TPG = 1 - } - } - if game.Paused { if inpututil.IsKeyJustPressed(ebiten.KeyN) { game.RunOneStep = true } } + + // Pane + if ebiten.IsKeyPressed(ebiten.KeyA) || ebiten.IsKeyPressed(ebiten.KeyArrowLeft) { + game.Camera.Position[0] -= 1 + } + if ebiten.IsKeyPressed(ebiten.KeyD) || ebiten.IsKeyPressed(ebiten.KeyArrowRight) { + game.Camera.Position[0] += 1 + } + if ebiten.IsKeyPressed(ebiten.KeyW) || ebiten.IsKeyPressed(ebiten.KeyArrowUp) { + game.Camera.Position[1] -= 1 + } + if ebiten.IsKeyPressed(ebiten.KeyS) || ebiten.IsKeyPressed(ebiten.KeyArrowDown) { + game.Camera.Position[1] += 1 + } + + // Zoom + _, dy := ebiten.Wheel() + if ebiten.IsKeyPressed(ebiten.KeyO) { + if game.Camera.ZoomFactor > -2400 { + game.Camera.ZoomFactor -= 1 + } + } + if ebiten.IsKeyPressed(ebiten.KeyI) { + if game.Camera.ZoomFactor < 2400 { + game.Camera.ZoomFactor += 1 + } + } + + step := 1 + if game.WheelTurned { + step = 50 + } else { + game.WheelTurned = false + } + + if dy < 0 { + if game.Camera.ZoomFactor > -2400 { + game.Camera.ZoomFactor -= step + } + } + + if dy > 0 { + if game.Camera.ZoomFactor < 2400 { + game.Camera.ZoomFactor += step + } + } + + if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { + game.Camera.Reset() + } + } func (game *Game) Update() error { @@ -257,10 +297,11 @@ func (game *Game) Draw(screen *ebiten.Image) { // themselfes will be 1px smaller as their nominal size, producing // a nice grey grid with grid lines op := &ebiten.DrawImageOptions{} + if game.NoGrid { - screen.Fill(game.White) + game.World.Fill(game.White) } else { - screen.Fill(game.Grey) + game.World.Fill(game.Grey) } for y := 0; y < game.Height; y++ { @@ -271,17 +312,21 @@ func (game *Game) Draw(screen *ebiten.Image) { switch game.Grids[game.Index].Data[y][x] { case 1: - screen.DrawImage(game.Tiles.Black, op) + game.World.DrawImage(game.Tiles.Black, op) case 0: if game.History.Data[y][x] == 1 && game.ShowEvolution { - screen.DrawImage(game.Tiles.Beige, op) + game.World.DrawImage(game.Tiles.Beige, op) } else { - screen.DrawImage(game.Tiles.White, op) + game.World.DrawImage(game.Tiles.White, op) } } } } + game.Camera.Render(game.World, screen) + + //worldX, worldY := game.Camera.ScreenToWorld(ebiten.CursorPosition()) + if game.Debug { paused := "" if game.Paused { @@ -395,6 +440,15 @@ func (game *Game) Init() { game.ScreenWidth = game.Cellsize * game.Width game.ScreenHeight = game.Cellsize * game.Height + game.Camera = Camera{ + ViewPort: f64.Vec2{ + float64(game.ScreenWidth), + float64(game.ScreenHeight), + }, + } + + game.World = ebiten.NewImage(game.ScreenWidth, game.ScreenHeight) + game.InitGrid() game.InitPattern() game.InitTiles() diff --git a/sample-rles/lightweight9.rle b/sample-rles/lightweight9.rle new file mode 100644 index 0000000..50867bb --- /dev/null +++ b/sample-rles/lightweight9.rle @@ -0,0 +1,4 @@ +x = 33, y = 10, rule = B3/S23 +5b3o17b3o$4bo3bo3bo7bo3bo3bo$3b2o4bob3o5b3obo4b2o$2bobob2obo3b2o3b2o3bob2obob +o$b2obo4bobo2b2ob2o2bobo4bob2o$o4bo3bo4bo3bo4bo3bo4bo$14b5o$2o9b2obo3bob2o9b +2o$11b2obo3bob2o$11b2obobobob2o!