Mi2dRPGamEng/engine/camera.py

62 lines
2.6 KiB
Python

from __future__ import annotations
ZOOM_LERP_RATE = 3.0 # per-second convergence rate for update_zoom's approach to the target
class OrthoCamera:
"""Top-down orthographic camera; world space is tile units with y increasing downward."""
def __init__(self, viewport_w: int, viewport_h: int, tiles_visible_y: float = 12.0):
self.x = 0.0
self.y = 0.0
self.viewport_w = viewport_w
self.viewport_h = viewport_h
self.tiles_visible_y = tiles_visible_y
self.target_tiles_visible_y = tiles_visible_y
def set_target(self, x: float, y: float) -> None:
self.x, self.y = x, y
def set_zoom_target(self, tiles_visible_y: float) -> None:
"""Where update_zoom should smoothly carry tiles_visible_y toward - e.g. a wider value
while steering a ship, so the view zooms out to take in more of the ship/surroundings.
"""
self.target_tiles_visible_y = tiles_visible_y
def update_zoom(self, dt: float) -> None:
"""Exponentially approaches target_tiles_visible_y - call once per frame with the
frame's dt. Snaps once the gap is negligible so it settles instead of creeping forever.
"""
diff = self.target_tiles_visible_y - self.tiles_visible_y
if abs(diff) < 1e-3:
self.tiles_visible_y = self.target_tiles_visible_y
return
self.tiles_visible_y += diff * min(1.0, ZOOM_LERP_RATE * dt)
def resize(self, viewport_w: int, viewport_h: int) -> None:
self.viewport_w, self.viewport_h = viewport_w, viewport_h
def _world_bounds(self) -> tuple[float, float, float, float]:
half_h = self.tiles_visible_y / 2
aspect = self.viewport_w / max(self.viewport_h, 1)
half_w = half_h * aspect
return self.x - half_w, self.x + half_w, self.y - half_h, self.y + half_h
def scale_offset(self) -> tuple[tuple[float, float], tuple[float, float]]:
"""Returns (scale, offset) such that ndc = world * scale + offset."""
left, right, top, bottom = self._world_bounds()
scale_x = 2.0 / (right - left)
scale_y = -2.0 / (bottom - top)
offset_x = -left * scale_x - 1.0
offset_y = 1.0 - top * scale_y
return (scale_x, scale_y), (offset_x, offset_y)
def screen_to_world(self, px: float, py: float) -> tuple[float, float]:
"""Unprojects a canvas logical-pixel position (0,0 top-left) into world tile coords."""
left, right, top, bottom = self._world_bounds()
wx = left + (px / max(self.viewport_w, 1)) * (right - left)
wy = top + (py / max(self.viewport_h, 1)) * (bottom - top)
return wx, wy