"""
This is the animation component module for game objects.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from os import path as os_path, walk
from .. import Component
from .... import Vector, Time, get_path, Draw, Camera, Surface
if TYPE_CHECKING:
from . import Spritesheet
[docs]class Animation(Component):
"""
Animations are a series of images that update automatically in accordance with parameters.
Args:
scale: The scale of the animation. Defaults to (1, 1).
fps: The frames per second of the animation. Defaults to 24.
af: Whether to use anisotropic filtering on the animation. Defaults to False.
flipx: Whether to flip the animation horizontally. Defaults to False.
flipy: Whether to flip the animation vertically. Defaults to False.
alpha: The alpha of the animation. Defaults to 255.
offset: The offset of the animation from the game object. Defaults to (0, 0).
rot_offset: The rotation offset of the animation from the game object. Defaults to 0.
z_index: The z-index of the animation. Defaults to 0.
hidden: Whether the animation is hidden. Defaults to False.
"""
def __init__(
self,
scale: Vector | tuple[float, float] = (1, 1),
fps: int = 24,
af: bool = False,
flipx: bool = False,
flipy: bool = False,
alpha: int = 255,
offset: Vector | tuple[float, float] = (0, 0),
rot_offset: float = 0,
z_index: int = 0,
hidden: bool = False,
):
super().__init__(offset=offset, rot_offset=rot_offset, z_index=z_index, hidden=hidden)
self._fps: int = fps
self.singular = False
self._states: dict[str, list[Surface]] = {}
self._freeze: int = -1
self.default_state: str = ""
"""The key of the default state."""
self.current_state: str = ""
"""The key of the current state."""
self.animation_frames_left: int = 0
"""The number of animation frames left."""
self._current_frame: int = 0
"""The current frame of the animation."""
self.loop: bool = True
"""Whether the animation should loop."""
self.scale: Vector = Vector.create(scale)
"""The scale of the animation."""
self.af: bool = af
"""Whether to enable anisotropic filtering."""
self.flipx: bool = flipx
"""Whether to flip the animation along the x axis."""
self.flipy: bool = flipy
"""Whether to flip the animation along the y axis."""
self.alpha: int = alpha
"""The alpha of the animation."""
self._time_step: float = 1 / self._fps
self._time_count: float = 0
@property
def fps(self):
"""The fps of the animation."""
return self._fps
@fps.setter
def fps(self, new):
self._fps = new
self._time_step = 1 / self._fps
@property
def current_frame(self) -> int:
"""The current frame that the animation is on."""
return self._current_frame
@current_frame.setter
def current_frame(self, new: int):
self._current_frame = new
self.animation_frames_left = len(self._states[self.current_state]) - (1 + self._current_frame)
[docs] def anim_frame(self) -> Surface:
"""The current animation frame."""
surface = self._states[self.current_state][self.current_frame]
surface.af = self.af
surface.rotation = self.true_rotation()
surface.set_alpha(self.alpha)
calculated_scale = self.scale.clone()
if self.flipx:
calculated_scale.x *= -1
if self.flipy:
calculated_scale.y *= -1
surface.scale = calculated_scale
if not surface.uptodate:
surface._regen()
return surface
[docs] def set_state(self, new_state: str, loop: bool = False, freeze: int = -1):
"""
Set the current animation state.
Args:
new_state: The key of the new current state.
loop: Whether to loop the state. Defaults to False.
freeze: Freezes the animation once the specified frame is reached (No animation).
Use -1 to never freeze. Defaults to -1.
Raises:
KeyError: The new_state key is not in the initialized states.
"""
if new_state != self.current_state:
if new_state not in self._states:
raise KeyError(f"The given state {new_state} is not in the initialized states")
self.loop = loop
self.current_state = new_state
self.reset()
self._freeze = freeze
[docs] def reset(self):
"""Reset the animation state back to the first frame."""
self.current_frame = 0
[docs] def add(self, state_name: str, images: list[Surface]):
"""
Adds a state to the animation.
Args:
state_name: The key used to reference this state.
images: A list of images to use as the animation.
"""
self._states[state_name] = images
if len(self._states) == 1:
self.default_state = state_name
self.current_state = state_name
self.reset()
[docs] def add_folder(self, state_name: str, path: str, recursive: bool = True):
"""
Adds a state from a folder of images. Directory must be solely comprised of images.
Args:
state_name: The key used to reference this state.
path: The relative path to the folder you wish to import
recursive: Whether it will import an animation shallowly or recursively. Defaults to True.
"""
ret_list = []
p = get_path(path)
if not recursive:
_, _, files = next(walk(p))
files.sort()
for image_path in files:
try:
path_to_image = os_path.join(p, image_path)
image = Surface.from_file(path_to_image)
ret_list.append(image)
except TypeError:
continue
else:
for _, _, files in walk(p):
# walk to directory path and ignore name and subdirectories
files.sort()
for image_path in files:
try:
path_to_image = os_path.join(p, image_path)
image = Surface.from_file(path_to_image)
ret_list.append(image)
except TypeError:
continue
self.add(state_name, ret_list)
[docs] def add_spritesheet(
self,
state_name: str,
spritesheet: Spritesheet,
from_coord: Vector | tuple[float, float] = (0, 0),
to_coord: Vector | tuple[float, float] = (0, 0)
):
"""
Adds a state from a spritesheet. Will include all sprites from the from_coord to the to_coord.
Args:
state_name: The key used to reference this state.
spritesheet: The spritesheet to use.
from_coord: The grid coordinate of the first frame. Defaults to (0, 0).
to_coord: The grid coordinate of the last coord. Defaults to (0, 0).
Example:
.. code-block:: python
animation.add_spritesheet("idle", spritesheet, Vector(0, 0), Vector(1, 3))
# This will add the frames (0, 0) to (0, size) and (1, 0) to (1, 3) inclusive to the animation
# with the state name "idle".
animation.add_spritesheet("idle", spritesheet, to_coord=spritesheet.end)
# This will just load from the start to the end of the spritesheet.
"""
state = []
x, y = int(from_coord[0]), int(from_coord[1])
to_x, to_y = int(to_coord[0]), int(to_coord[1])
while True:
state.append(spritesheet.get(x, y))
if y == to_y and x == to_x:
break
x += 1
if x >= spritesheet.grid_size.x:
x = 0
if y >= spritesheet.grid_size.y:
break
y += 1
self.add(state_name, state)
[docs] def update(self):
"""Steps the animation forwards."""
self._time_count += Time.delta_time
while self._time_count > self._time_step:
if self.current_frame != self._freeze:
if self.animation_frames_left > 0:
self.current_frame += 1
elif self.loop:
self.reset()
else:
self.set_state(self.default_state, True)
self._time_count -= self._time_step
[docs] def draw(self, camera: Camera):
"""Draws the animation frame."""
Draw.queue_surface(self.anim_frame(), self.true_pos(), self.true_z(), camera)
[docs] def clone(self) -> Animation:
"""Clones the animation."""
new = Animation(
scale=self.scale.clone(),
fps=self.fps,
af=self.af,
flipx=self.flipx,
flipy=self.flipy,
offset=self.offset.clone(),
rot_offset=self.rot_offset,
z_index=self.z_index,
)
new_states = {}
for state, frames in self._states.items():
new_states[state] = [frame.clone() for frame in frames]
new._states = new_states
new.default_state = self.default_state
new.current_state = self.current_state
new.loop = self.loop
new.current_frame = self.current_frame
new._freeze = self._freeze
return new