added zooming + paning

This commit is contained in:
2024-05-22 19:01:58 +02:00
parent cc17500a46
commit a5f4657d18
5 changed files with 149 additions and 25 deletions

68
camera.go Normal file
View File

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

2
go.mod
View File

@@ -3,9 +3,9 @@ module github.com/tlinden/gameoflife
go 1.22 go 1.22
require ( require (
github.com/alecthomas/repr v0.4.0
github.com/hajimehoshi/ebiten/v2 v2.7.4 github.com/hajimehoshi/ebiten/v2 v2.7.4
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
golang.org/x/image v0.16.0
) )
require ( require (

2
go.sum
View File

@@ -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 h1:48bCqKTuD7Z0UovDfvpCn7wZ0GUZ+yosIteNDthn3FU=
github.com/ebitengine/gomobile v0.0.0-20240518074828-e86332849895/go.mod h1:XZdLv05c5hOZm3fM2NlJ92FyEZjnslcMcNRrhxs8+8M= 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= github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE=

98
main.go
View File

@@ -11,6 +11,7 @@ import (
"strings" "strings"
"github.com/tlinden/gameoflife/rle" "github.com/tlinden/gameoflife/rle"
"golang.org/x/image/math/f64"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil" "github.com/hajimehoshi/ebiten/v2/ebitenutil"
@@ -48,6 +49,9 @@ type Game struct {
Rule *Rule // which rule to use, default: B3/S23 Rule *Rule // which rule to use, default: B3/S23
Tiles Images // pre-computed tiles for dead and alife cells Tiles Images // pre-computed tiles for dead and alife cells
RLE *rle.RLE // loaded GOL pattern from RLE file 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) { 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 game.Paused = true // drawing while running makes no sense
} }
if inpututil.IsKeyJustPressed(ebiten.KeyArrowDown) { if ebiten.IsKeyPressed(ebiten.KeyPageDown) {
if game.TPG < 120 { if game.TPG < 120 {
game.TPG++ game.TPG++
} }
} }
if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) { if ebiten.IsKeyPressed(ebiten.KeyPageUp) {
if game.TPG > 1 { if game.TPG > 1 {
game.TPG-- 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 game.Paused {
if inpututil.IsKeyJustPressed(ebiten.KeyN) { if inpututil.IsKeyJustPressed(ebiten.KeyN) {
game.RunOneStep = true 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 { 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 // themselfes will be 1px smaller as their nominal size, producing
// a nice grey grid with grid lines // a nice grey grid with grid lines
op := &ebiten.DrawImageOptions{} op := &ebiten.DrawImageOptions{}
if game.NoGrid { if game.NoGrid {
screen.Fill(game.White) game.World.Fill(game.White)
} else { } else {
screen.Fill(game.Grey) game.World.Fill(game.Grey)
} }
for y := 0; y < game.Height; y++ { 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] { switch game.Grids[game.Index].Data[y][x] {
case 1: case 1:
screen.DrawImage(game.Tiles.Black, op) game.World.DrawImage(game.Tiles.Black, op)
case 0: case 0:
if game.History.Data[y][x] == 1 && game.ShowEvolution { if game.History.Data[y][x] == 1 && game.ShowEvolution {
screen.DrawImage(game.Tiles.Beige, op) game.World.DrawImage(game.Tiles.Beige, op)
} else { } 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 { if game.Debug {
paused := "" paused := ""
if game.Paused { if game.Paused {
@@ -395,6 +440,15 @@ func (game *Game) Init() {
game.ScreenWidth = game.Cellsize * game.Width game.ScreenWidth = game.Cellsize * game.Width
game.ScreenHeight = game.Cellsize * game.Height 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.InitGrid()
game.InitPattern() game.InitPattern()
game.InitTiles() game.InitTiles()

View File

@@ -0,0 +1,4 @@
x = 33, y = 10, rule = B3/S23
5b3o17b3o$4bo3bo3bo7bo3bo3bo$3b2o4bob3o5b3obo4b2o$2bobob2obo3b2o3b2o3bob2obob
o$b2obo4bobo2b2ob2o2bobo4bob2o$o4bo3bo4bo3bo4bo3bo4bo$14b5o$2o9b2obo3bob2o9b
2o$11b2obo3bob2o$11b2obobobob2o!