Source code for simvx.core.nodes_2d.camera

"""Camera2D -- 2D camera with smoothing, zoom, bounds, and shake."""

from __future__ import annotations

import random

from ..descriptors import Property
from ..math.types import Vec2, clamp
from .node2d import Node2D


[docs] class Camera2D(Node2D): """2D camera for scrolling games. Provides target following with smoothing, zoom, viewport bounds/limits, and screen shake effects. The active Camera2D's offset is applied by the renderer to all 2D draw calls. """ zoom = Property(1.0, range=(0.1, 10.0), hint="Camera zoom level", group="Camera") smoothing = Property(0.0, range=(0.0, 50.0), hint="Lerp speed (0 = instant)", group="Camera") limit_left = Property(-1e9, hint="Left edge limit", group="Camera") limit_right = Property(1e9, hint="Right edge limit", group="Camera") limit_top = Property(-1e9, hint="Top edge limit", group="Camera") limit_bottom = Property(1e9, hint="Bottom edge limit", group="Camera") cull_mask = Property( 0xFFFFFFFF, range=(0, 0xFFFFFFFF), hint="Cull mask bitmask (32 layers, all visible by default)", group="Camera", )
[docs] def set_cull_mask_layer(self, index: int, enabled: bool = True) -> None: """Enable or disable a specific cull mask layer (0-31).""" if not 0 <= index < 32: raise ValueError(f"Cull mask layer index must be 0-31, got {index}") if enabled: self.cull_mask = self.cull_mask | (1 << index) else: self.cull_mask = self.cull_mask & ~(1 << index)
[docs] def is_cull_mask_layer_enabled(self, index: int) -> bool: """Check if a specific cull mask layer is enabled (0-31).""" if not 0 <= index < 32: raise ValueError(f"Cull mask layer index must be 0-31, got {index}") return bool(self.cull_mask & (1 << index))
def __init__(self, **kwargs): super().__init__(**kwargs) self.target: Node2D | None = None self.offset = Vec2() self._shake_intensity = 0.0 self._shake_duration = 0.0 self._shake_timer = 0.0 self._current = Vec2()
[docs] def ready(self): if self._tree: self._tree._current_camera_2d = self if self.target: self._current = Vec2(self.target.world_position)
[docs] def shake(self, intensity: float = 5.0, duration: float = 0.3): """Start a screen shake effect.""" self._shake_intensity = intensity self._shake_duration = duration self._shake_timer = duration
[docs] def process(self, dt: float): target_pos = self.target.world_position if self.target else self.world_position if self.smoothing > 0 and dt > 0: t = min(1.0, self.smoothing * dt) self._current = self._current + (target_pos - self._current) * t else: self._current = Vec2(target_pos) cam_x = clamp(self._current.x, self.limit_left, self.limit_right) cam_y = clamp(self._current.y, self.limit_top, self.limit_bottom) self._current = Vec2(cam_x, cam_y) shake_offset = Vec2() if self._shake_timer > 0: self._shake_timer -= dt fade = self._shake_timer / self._shake_duration if self._shake_duration > 0 else 0 shake_offset = Vec2( random.uniform(-1, 1) * self._shake_intensity * fade, random.uniform(-1, 1) * self._shake_intensity * fade, ) self.offset = (self._current * -1 + shake_offset) * self.zoom
[docs] def world_to_screen(self, world_pos: Vec2, screen_size: Vec2) -> Vec2: """Convert a world position to screen coordinates.""" return (world_pos + self.offset) + screen_size * 0.5
[docs] def screen_to_world(self, screen_pos: Vec2, screen_size: Vec2) -> Vec2: """Convert screen coordinates to world position.""" return (screen_pos - screen_size * 0.5 - self.offset) * (1.0 / self.zoom)