62 lines
2.6 KiB
Python
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
|