"""
Camera class
"""
import math
from typing import Optional, Tuple
from pyglet.math import Mat4, Vec2, Vec3
import arcade
[docs]class Camera:
"""
The Camera class is used for controlling the visible viewport.
It is very useful for separating a scrolling screen of sprites, and a GUI overlay.
For an example of this in action, see :ref:`sprite_move_scrolling`.
:param int viewport_width: Width of the viewport. If not set the window width will be used.
:param int viewport_height: Height of the viewport. If not set the window height will be used.
:param Window window: Window to associate with this camera, if working with a multi-window program.
"""
def __init__(
self,
viewport_width: int = 0,
viewport_height: int = 0,
window: Optional["arcade.Window"] = None,
):
# Reference to Context, used to update projection matrix
self._window = window or arcade.get_window()
# Position
self.position = Vec2(0, 0)
self.goal_position = Vec2(0, 0)
self._rotation = 0.0
self._anchor: Optional[Tuple[float, float]] = None
# Movement Speed, 1.0 is instant
self.move_speed = 1.0
# Matrixes
# Projection Matrix is used to apply the camera viewport size
self.projection_matrix = None
# View Matrix is what the camera is looking at(position)
self.view_matrix = None
# We multiple projection and view matrices to get combined, this is what actually gets sent to GL context
self.combined_matrix = None
# Near and Far
self.near = -1
self.far = 1
# Shake
self.shake_velocity = Vec2()
self.shake_offset = Vec2()
self.shake_speed = 0.0
self.shake_damping = 0.0
self.scale = 1.0
self.viewport_width = viewport_width or self._window.width
self.viewport_height = viewport_height or self._window.height
self.set_projection()
@property
def rotation(self) -> float:
"""
Get or set the rotation in degrees.
This will rotate the camera clockwise meaning
the contents will rotate counter-clockwise.
"""
return self._rotation
@rotation.setter
def rotation(self, value: float):
self._rotation = value
@property
def anchor(self) -> Optional[Tuple[float, float]]:
"""
Get or set the rotation anchor for the camera.
By default, the anchor is the center of the screen
and the anchor value is `None`. Assigning a custom
anchor point will override this behavior.
The anchor point is in world / global coordinates.
Example::
# Set the anchor to the center of the world
camera.anchor = 0, 0
# Set the anchor to the center of the player
camera.anchor = player.position
"""
return self._anchor
@anchor.setter
def anchor(self, anchor: Optional[Tuple[float, float]]):
self._anchor = None if anchor is None else (anchor[0], anchor[1])
[docs] def update(self):
"""
Update the camera's viewport to the current settings.
"""
# Apply Goal Position
self.position = self.position.lerp(self.goal_position, self.move_speed)
# Apply Camera Shake
# Move our offset based on shake velocity
self.shake_offset += self.shake_velocity
# Get x and ys
vx = self.shake_velocity[0]
vy = self.shake_velocity[1]
ox = self.shake_offset[0]
oy = self.shake_offset[1]
# Calculate the angle our offset is at, and how far out
angle = math.atan2(ox, oy)
distance = arcade.get_distance(0, 0, ox, oy)
velocity_mag = arcade.get_distance(0, 0, vx, vy)
# Ok, what's the reverse? Pull it back in.
reverse_speed = min(self.shake_speed, distance)
opposite_angle = angle + math.pi
opposite_vector = Vec2(
math.sin(opposite_angle) * reverse_speed,
math.cos(opposite_angle) * reverse_speed,
)
# Shaking almost done? Zero it out
if velocity_mag < self.shake_speed and distance < self.shake_speed:
self.shake_velocity = Vec2(0, 0)
self.shake_offset = Vec2(0, 0)
# Come up with a new velocity, pulled by opposite vector and damped
self.shake_velocity += opposite_vector
self.shake_velocity *= Vec2(self.shake_damping, self.shake_damping)
# Figure out our 'real' position plus the shake
result_position = self.position + self.shake_offset
result_position = Vec3(
(result_position[0] / ((self.viewport_width * self.scale) / 2)),
(result_position[1] / ((self.viewport_height * self.scale) / 2)),
0
)
self.view_matrix = ~(Mat4.from_translation(result_position) @ Mat4().scale(
(self.scale, self.scale, 1.0)))
self.combined_matrix = self.projection_matrix @ self.view_matrix
[docs] def set_projection(self):
"""
Update the projection matrix of the camera. This creates an orthogonal
projection based on the viewport size of the camera.
"""
self.projection_matrix = Mat4.orthogonal_projection(
0,
self.scale * self.viewport_width,
0,
self.scale * self.viewport_height,
self.near,
self.far
)
[docs] def resize(self, viewport_width: int, viewport_height: int):
"""
Resize the camera's viewport. Call this when the window resizes.
:param int viewport_width: Width of the viewport
:param int viewport_height: Height of the viewport
"""
self.viewport_width = viewport_width
self.viewport_height = viewport_height
self.set_projection()
[docs] def shake(self, velocity: Vec2, speed: float = 1.5, damping: float = 0.9):
"""
Add a camera shake.
:param Vec2 velocity: Vector to start moving the camera
:param float speed: How fast to shake
:param float damping: How fast to stop shaking
"""
if not isinstance(velocity, Vec2):
velocity = Vec2(*velocity)
self.shake_velocity += velocity
self.shake_speed = speed
self.shake_damping = damping
[docs] def move_to(self, vector: Vec2, speed: float = 1.0):
"""
Sets the goal position of the camera.
The camera will lerp towards this position based on the provided speed,
updating its position every time the use() function is called.
:param Vec2 vector: Vector to move the camera towards.
:param Vec2 speed: How fast to move the camera, 1.0 is instant, 0.1 moves slowly
"""
pos = Vec2(vector[0], vector[1])
self.goal_position = pos
self.move_speed = speed
[docs] def move(self, vector: Vec2):
"""
Moves the camera with a speed of 1.0, aka instant move
This is equivalent to calling move_to(my_pos, 1.0)
"""
self.move_to(vector, 1.0)
[docs] def zoom(self, change: float):
"""
Zoom the camera in or out. Or not.
This will currently raise an error
TODO implement
"""
raise NotImplementedError("Zooming the camera is currently un-supported, but will be in a later release.")
[docs] def use(self):
"""
Select this camera for use. Do this right before you draw.
"""
self._window.current_camera = self
self.update()
# Viewport / projection
self._window.ctx.viewport = 0, 0, int(self.viewport_width), int(self.viewport_height)
self._window.ctx.projection_2d_matrix = self.combined_matrix
# View matrix for rotation
rotate = Mat4.from_rotation(math.radians(self._rotation), (0, 0, 1))
# If no anchor is set, use the center of the screen
if self._anchor is None:
offset = Vec3(self.position.x, self.position.y, 0)
offset += Vec3(self.viewport_width / 2, self.viewport_height / 2, 0)
else:
offset = Vec3(self._anchor[0], self._anchor[1], 0)
translate_pre = Mat4.from_translation(offset)
translate_post = Mat4.from_translation(-offset)
self._window.ctx.view_matrix_2d = translate_post @ rotate @ translate_pre