Files
sideline/engine/camera.py
David Gwilliam 9b139a40f7 feat(core): add Camera abstraction for viewport scrolling
- Add Camera class with modes: vertical, horizontal, omni, floating
- Refactor scroll.py and demo to use Camera abstraction
- Add vis_offset for horizontal scrolling support
- Add camera_x to EffectContext for effects
- Add pygame window resize handling
- Add HUD effect plugin for demo mode
- Add --demo flag to run demo mode
- Add tests for Camera and vis_offset
2026-03-16 01:46:21 -07:00

110 lines
3.2 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
"""
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 Camera:
"""Camera for viewport scrolling.
Attributes:
x: Current horizontal offset (positive = scroll left)
y: Current vertical offset (positive = scroll up)
mode: Current camera mode
speed: Base scroll speed
custom_update: Optional custom update function
"""
x: int = 0
y: int = 0
mode: CameraMode = CameraMode.VERTICAL
speed: float = 1.0
custom_update: Callable[["Camera", float], None] | None = None
_time: float = field(default=0.0, repr=False)
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)
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
@classmethod
def vertical(cls, speed: float = 1.0) -> "Camera":
"""Create a vertical scrolling camera."""
return cls(mode=CameraMode.VERTICAL, speed=speed)
@classmethod
def horizontal(cls, speed: float = 1.0) -> "Camera":
"""Create a horizontal scrolling camera."""
return cls(mode=CameraMode.HORIZONTAL, speed=speed)
@classmethod
def omni(cls, speed: float = 1.0) -> "Camera":
"""Create an omnidirectional scrolling camera."""
return cls(mode=CameraMode.OMNI, speed=speed)
@classmethod
def floating(cls, speed: float = 1.0) -> "Camera":
"""Create a floating/bobbing camera."""
return cls(mode=CameraMode.FLOATING, speed=speed)
@classmethod
def custom(cls, update_fn: Callable[["Camera", float], None]) -> "Camera":
"""Create a camera with custom update function."""
return cls(custom_update=update_fn)