forked from genewildish/Mainline
- Add Canvas class for 2D surface management - Add CanvasStage for pipeline integration - Add FontStage as Transform for font rendering - Update Camera with x, y, w, h, zoom and guardrails - Add get_dimensions() to Display protocol
219 lines
6.3 KiB
Python
219 lines
6.3 KiB
Python
"""
|
|
Camera system for viewport scrolling.
|
|
|
|
Provides abstraction for camera motion in different modes:
|
|
- Vertical: traditional upward scroll
|
|
- Horizontal: left/right movement
|
|
- Omni: combination of both
|
|
- Floating: sinusoidal/bobbing motion
|
|
|
|
The camera defines a visible viewport into a larger Canvas.
|
|
"""
|
|
|
|
import math
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum, auto
|
|
|
|
|
|
class CameraMode(Enum):
|
|
VERTICAL = auto()
|
|
HORIZONTAL = auto()
|
|
OMNI = auto()
|
|
FLOATING = auto()
|
|
|
|
|
|
@dataclass
|
|
class CameraViewport:
|
|
"""Represents the visible viewport."""
|
|
|
|
x: int
|
|
y: int
|
|
width: int
|
|
height: int
|
|
|
|
|
|
@dataclass
|
|
class Camera:
|
|
"""Camera for viewport scrolling.
|
|
|
|
The camera defines a visible viewport into a Canvas.
|
|
It can be smaller than the canvas to allow scrolling,
|
|
and supports zoom to scale the view.
|
|
|
|
Attributes:
|
|
x: Current horizontal offset (positive = scroll left)
|
|
y: Current vertical offset (positive = scroll up)
|
|
mode: Current camera mode
|
|
speed: Base scroll speed
|
|
zoom: Zoom factor (1.0 = 100%, 2.0 = 200% zoom out)
|
|
canvas_width: Width of the canvas being viewed
|
|
canvas_height: Height of the canvas being viewed
|
|
custom_update: Optional custom update function
|
|
"""
|
|
|
|
x: int = 0
|
|
y: int = 0
|
|
mode: CameraMode = CameraMode.VERTICAL
|
|
speed: float = 1.0
|
|
zoom: float = 1.0
|
|
canvas_width: int = 200 # Larger than viewport for scrolling
|
|
canvas_height: int = 200
|
|
custom_update: Callable[["Camera", float], None] | None = None
|
|
_time: float = field(default=0.0, repr=False)
|
|
|
|
@property
|
|
def w(self) -> int:
|
|
"""Shorthand for viewport_width."""
|
|
return self.viewport_width
|
|
|
|
@property
|
|
def h(self) -> int:
|
|
"""Shorthand for viewport_height."""
|
|
return self.viewport_height
|
|
|
|
@property
|
|
def viewport_width(self) -> int:
|
|
"""Get the visible viewport width.
|
|
|
|
This is the canvas width divided by zoom.
|
|
"""
|
|
return max(1, int(self.canvas_width / self.zoom))
|
|
|
|
@property
|
|
def viewport_height(self) -> int:
|
|
"""Get the visible viewport height.
|
|
|
|
This is the canvas height divided by zoom.
|
|
"""
|
|
return max(1, int(self.canvas_height / self.zoom))
|
|
|
|
def get_viewport(self) -> CameraViewport:
|
|
"""Get the current viewport bounds.
|
|
|
|
Returns:
|
|
CameraViewport with position and size (clamped to canvas bounds)
|
|
"""
|
|
vw = self.viewport_width
|
|
vh = self.viewport_height
|
|
|
|
clamped_x = max(0, min(self.x, self.canvas_width - vw))
|
|
clamped_y = max(0, min(self.y, self.canvas_height - vh))
|
|
|
|
return CameraViewport(
|
|
x=clamped_x,
|
|
y=clamped_y,
|
|
width=vw,
|
|
height=vh,
|
|
)
|
|
|
|
def set_zoom(self, zoom: float) -> None:
|
|
"""Set the zoom factor.
|
|
|
|
Args:
|
|
zoom: Zoom factor (1.0 = 100%, 2.0 = zoomed out 2x, 0.5 = zoomed in 2x)
|
|
"""
|
|
self.zoom = max(0.1, min(10.0, zoom))
|
|
|
|
def update(self, dt: float) -> None:
|
|
"""Update camera position based on mode.
|
|
|
|
Args:
|
|
dt: Delta time in seconds
|
|
"""
|
|
self._time += dt
|
|
|
|
if self.custom_update:
|
|
self.custom_update(self, dt)
|
|
return
|
|
|
|
if self.mode == CameraMode.VERTICAL:
|
|
self._update_vertical(dt)
|
|
elif self.mode == CameraMode.HORIZONTAL:
|
|
self._update_horizontal(dt)
|
|
elif self.mode == CameraMode.OMNI:
|
|
self._update_omni(dt)
|
|
elif self.mode == CameraMode.FLOATING:
|
|
self._update_floating(dt)
|
|
|
|
self._clamp_to_bounds()
|
|
|
|
def _clamp_to_bounds(self) -> None:
|
|
"""Clamp camera position to stay within canvas bounds.
|
|
|
|
Only clamps if the viewport is smaller than the canvas.
|
|
If viewport equals canvas (no scrolling needed), allows any position
|
|
for backwards compatibility with original behavior.
|
|
"""
|
|
vw = self.viewport_width
|
|
vh = self.viewport_height
|
|
|
|
# Only clamp if there's room to scroll
|
|
if vw < self.canvas_width:
|
|
self.x = max(0, min(self.x, self.canvas_width - vw))
|
|
if vh < self.canvas_height:
|
|
self.y = max(0, min(self.y, self.canvas_height - vh))
|
|
|
|
def _update_vertical(self, dt: float) -> None:
|
|
self.y += int(self.speed * dt * 60)
|
|
|
|
def _update_horizontal(self, dt: float) -> None:
|
|
self.x += int(self.speed * dt * 60)
|
|
|
|
def _update_omni(self, dt: float) -> None:
|
|
speed = self.speed * dt * 60
|
|
self.y += int(speed)
|
|
self.x += int(speed * 0.5)
|
|
|
|
def _update_floating(self, dt: float) -> None:
|
|
base = self.speed * 30
|
|
self.y = int(math.sin(self._time * 2) * base)
|
|
self.x = int(math.cos(self._time * 1.5) * base * 0.5)
|
|
|
|
def reset(self) -> None:
|
|
"""Reset camera position."""
|
|
self.x = 0
|
|
self.y = 0
|
|
self._time = 0.0
|
|
self.zoom = 1.0
|
|
|
|
def set_canvas_size(self, width: int, height: int) -> None:
|
|
"""Set the canvas size and clamp position if needed.
|
|
|
|
Args:
|
|
width: New canvas width
|
|
height: New canvas height
|
|
"""
|
|
self.canvas_width = width
|
|
self.canvas_height = height
|
|
self._clamp_to_bounds()
|
|
|
|
@classmethod
|
|
def vertical(cls, speed: float = 1.0) -> "Camera":
|
|
"""Create a vertical scrolling camera."""
|
|
return cls(mode=CameraMode.VERTICAL, speed=speed, canvas_height=200)
|
|
|
|
@classmethod
|
|
def horizontal(cls, speed: float = 1.0) -> "Camera":
|
|
"""Create a horizontal scrolling camera."""
|
|
return cls(mode=CameraMode.HORIZONTAL, speed=speed, canvas_width=200)
|
|
|
|
@classmethod
|
|
def omni(cls, speed: float = 1.0) -> "Camera":
|
|
"""Create an omnidirectional scrolling camera."""
|
|
return cls(
|
|
mode=CameraMode.OMNI, speed=speed, canvas_width=200, canvas_height=200
|
|
)
|
|
|
|
@classmethod
|
|
def floating(cls, speed: float = 1.0) -> "Camera":
|
|
"""Create a floating/bobbing camera."""
|
|
return cls(
|
|
mode=CameraMode.FLOATING, speed=speed, canvas_width=200, canvas_height=200
|
|
)
|
|
|
|
@classmethod
|
|
def custom(cls, update_fn: Callable[["Camera", float], None]) -> "Camera":
|
|
"""Create a camera with custom update function."""
|
|
return cls(custom_update=update_fn)
|