aboutsummaryrefslogtreecommitdiff
path: root/engine/camera.go
diff options
context:
space:
mode:
Diffstat (limited to 'engine/camera.go')
-rw-r--r--engine/camera.go174
1 files changed, 174 insertions, 0 deletions
diff --git a/engine/camera.go b/engine/camera.go
new file mode 100644
index 0000000..867aaf4
--- /dev/null
+++ b/engine/camera.go
@@ -0,0 +1,174 @@
+package engine
+
+import (
+ "math"
+
+ "age_of_empires/physics"
+
+ "github.com/hajimehoshi/ebiten/v2"
+ "github.com/hajimehoshi/ebiten/v2/inpututil"
+)
+
+const (
+ ScreenWidth = 960
+ ScreenHeight = 640
+ WorldWidth = 3200
+ WorldHeight = 2400
+
+ cameraZoomMin = 0.25
+ cameraZoomMax = 3.0
+ cameraZoomStep = 0.10
+ cameraScrollSpeed = 400.0
+ cameraDrag = 8.0
+)
+
+type Camera struct {
+ X, Y float64
+ Zoom float64
+
+ vx, vy float64
+
+ dragActive bool
+ dragStartScreenX, dragStartScreenY int
+ dragStartCamX, dragStartCamY float64
+}
+
+func (c *Camera) WorldToScreen(p physics.Point) (float64, float64) {
+ sx := (float64(p.X)-c.X)*c.Zoom + ScreenWidth/2
+ sy := (float64(p.Y)-c.Y)*c.Zoom + ScreenHeight/2
+ return sx, sy
+}
+
+func (c *Camera) ScreenToWorld(sx, sy int) physics.Point {
+ zoom := c.Zoom
+ if zoom == 0 {
+ zoom = 1
+ }
+ wx := float64(sx-ScreenWidth/2)/zoom + c.X
+ wy := float64(sy-ScreenHeight/2)/zoom + c.Y
+ return physics.Point{X: int(math.Round(wx)), Y: int(math.Round(wy))}
+}
+
+func (c *Camera) GeoM() ebiten.GeoM {
+ var m ebiten.GeoM
+ m.Translate(-c.X, -c.Y)
+ m.Scale(c.Zoom, c.Zoom)
+ m.Translate(ScreenWidth/2, ScreenHeight/2)
+ return m
+}
+
+func (c *Camera) clampToBounds() {
+ halfW := (ScreenWidth / 2.0) / c.Zoom
+ halfH := (ScreenHeight / 2.0) / c.Zoom
+
+ minX, maxX := halfW, float64(WorldWidth)-halfW
+ if minX > maxX {
+ minX = WorldWidth / 2
+ maxX = WorldWidth / 2
+ }
+ minY, maxY := halfH, float64(WorldHeight)-halfH
+ if minY > maxY {
+ minY = WorldHeight / 2
+ maxY = WorldHeight / 2
+ }
+
+ if c.X < minX {
+ c.X = minX
+ if c.vx < 0 {
+ c.vx = 0
+ }
+ }
+ if c.X > maxX {
+ c.X = maxX
+ if c.vx > 0 {
+ c.vx = 0
+ }
+ }
+ if c.Y < minY {
+ c.Y = minY
+ if c.vy < 0 {
+ c.vy = 0
+ }
+ }
+ if c.Y > maxY {
+ c.Y = maxY
+ if c.vy > 0 {
+ c.vy = 0
+ }
+ }
+}
+
+func (c *Camera) Update(dt float64) {
+ zoom := c.Zoom
+ if zoom == 0 {
+ zoom = 1
+ }
+
+ // Scroll wheel zoom toward cursor
+ _, wy := ebiten.Wheel()
+ if wy != 0 {
+ mx, my := ebiten.CursorPosition()
+ // cursor world pos before zoom
+ wx0 := float64(mx-ScreenWidth/2)/zoom + c.X
+ wy0 := float64(my-ScreenHeight/2)/zoom + c.Y
+
+ newZoom := math.Max(cameraZoomMin, math.Min(cameraZoomMax, zoom+wy*cameraZoomStep))
+ c.Zoom = newZoom
+ zoom = newZoom
+
+ // translate so cursor stays fixed
+ wx1 := float64(mx-ScreenWidth/2)/zoom + c.X
+ wy1 := float64(my-ScreenHeight/2)/zoom + c.Y
+ c.X -= wx1 - wx0
+ c.Y -= wy1 - wy0
+ }
+
+ // Middle-mouse drag
+ mx, my := ebiten.CursorPosition()
+ if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonMiddle) {
+ c.dragActive = true
+ c.dragStartScreenX = mx
+ c.dragStartScreenY = my
+ c.dragStartCamX = c.X
+ c.dragStartCamY = c.Y
+ }
+ if c.dragActive {
+ if ebiten.IsMouseButtonPressed(ebiten.MouseButtonMiddle) {
+ c.X = c.dragStartCamX - float64(mx-c.dragStartScreenX)/zoom
+ c.Y = c.dragStartCamY - float64(my-c.dragStartScreenY)/zoom
+ } else {
+ c.dragActive = false
+ }
+ }
+
+ // Arrow key pan (velocity-based)
+ dx, dy := 0.0, 0.0
+ if ebiten.IsKeyPressed(ebiten.KeyArrowLeft) {
+ dx -= 1
+ }
+ if ebiten.IsKeyPressed(ebiten.KeyArrowRight) {
+ dx += 1
+ }
+ if ebiten.IsKeyPressed(ebiten.KeyArrowUp) {
+ dy -= 1
+ }
+ if ebiten.IsKeyPressed(ebiten.KeyArrowDown) {
+ dy += 1
+ }
+ if dx != 0 && dy != 0 {
+ inv := 1.0 / math.Sqrt2
+ dx *= inv
+ dy *= inv
+ }
+ c.vx += dx * cameraScrollSpeed
+ c.vy += dy * cameraScrollSpeed
+
+ // Apply velocity and exponential drag
+ c.X += c.vx * dt
+ c.Y += c.vy * dt
+ decay := math.Exp(-cameraDrag * dt)
+ c.vx *= decay
+ c.vy *= decay
+
+ c.clampToBounds()
+}