from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING, Literal
from PIL import Image
from PIL.Image import Transpose
from arcade.resources import resolve
# from arcade import Texture
from arcade.texture import Texture
from arcade.types.rect import Rect
if TYPE_CHECKING:
from arcade.hitbox import HitBoxAlgorithm
OriginChoices = Literal["upper_left", "lower_left"]
[docs]
class SpriteSheet:
"""
A sprite sheet is a single image containing multiple smaller images, or
frames. The class is used to load the image providing methods to slice out
parts of the image as separate images or textures.
Note that the default coordinate system used for slicing is using image coordinates
(0, 0) in the upper left corner. This matches the coordinate system used by PIL.
Args:
path Path to the image to load.
image: PIL image to use.
"""
def __init__(
self,
path: str | Path | None = None,
image: Image.Image | None = None,
):
self._path = None
if path:
self._path = resolve(path)
self._image = Image.open(self._path).convert("RGBA")
elif image:
self._image = image
else:
raise ValueError("Must provide either path or image")
self._flip_flags = (False, False)
[docs]
@classmethod
def from_image(cls, image: Image.Image) -> SpriteSheet:
"""
Create a sprite sheet from a PIL image.
Args:
image: PIL image to use.
"""
return cls(image=image)
@property
def image(self) -> Image.Image:
"""
Get or set the PIL image for this sprite sheet.
"""
return self._image
@image.setter
def image(self, image: Image.Image):
self._image = image
@property
def path(self) -> Path | None:
"""
The path to the sprite sheet if it was loaded from disk.
"""
return self._path
@property
def flip_flags(self) -> tuple[bool, bool]:
"""
Query the orientation of the sprite sheet.
This can be used to determine if the sprite sheet needs to be flipped.
Default values are ``(False, False)``. Will be modified when
:py:meth:`flip_left_right` or :py:meth:`flip_top_bottom` is called.
Tuple of booleans ``(flip_left_right, flip_top_bottom)``.
"""
return self._flip_flags
[docs]
def flip_left_right(self) -> None:
"""
Flips the internal image left to right.
"""
self._image = self._image.transpose(Transpose.FLIP_LEFT_RIGHT)
self._flip_flags = (not self._flip_flags[0], self._flip_flags[1])
[docs]
def flip_top_bottom(self) -> None:
"""
Flip the internal image top to bottom.
"""
self._image = self._image.transpose(Transpose.FLIP_TOP_BOTTOM)
self._flip_flags = (self._flip_flags[0], not self._flip_flags[1])
[docs]
def get_image(self, rect: Rect, y_up=False) -> Image.Image:
"""
Slice out an image from the sprite sheet.
Args:
rect:
The rectangle to crop out.
y_up:
Sets the coordinate space of the image to assert (0, 0)
in the bottom left.
"""
# PIL box is a 4-tuple: left, upper, right, and lower
if y_up:
return self.image.crop(
(
rect.left,
self.image.height - rect.bottom - rect.height,
rect.right,
self.image.height - rect.bottom,
)
)
else:
return self.image.crop(
(
rect.left,
rect.bottom,
rect.right,
rect.top,
)
)
# slice an image out of the sprite sheet
[docs]
def get_texture(
self, rect: Rect, hit_box_algorithm: HitBoxAlgorithm | None = None, y_up=False
) -> Texture:
"""
Slice out texture from the sprite sheet.
Args:
rect:
The rectangle to crop out.
hit_box_algorithm:
Hit box algorithm to use for the texture.
If not provided, the default hit box algorithm will be used.
y_up:
Sets the coordinate space of the image to assert (0, 0)
in the bottom left.
"""
im = self.get_image(rect, y_up)
texture = Texture(im, hit_box_algorithm=hit_box_algorithm)
texture.file_path = self._path
texture.crop_values = rect.lbwh_int
return texture
[docs]
def get_image_grid(
self,
size: tuple[int, int],
columns: int,
count: int,
margin: tuple[int, int, int, int] = (0, 0, 0, 0),
) -> list[Image.Image]:
"""
Slice a grid of textures from the sprite sheet.
Args:
size:
Size of each texture ``(width, height)``
columns:
Number of columns in the grid
count:
Number of textures to crop
margin:
The margin around each texture ``(left, right, bottom, top)``
"""
images = []
width, height = size
left, right, bottom, top = margin
for sprite_no in range(count):
row = sprite_no // columns
column = sprite_no % columns
x = (width + left + right) * column
y = (height + top + bottom) * row
im = self.image.crop((x, y, x + width, y + height))
images.append(im)
return images
[docs]
def get_texture_grid(
self,
size: tuple[int, int],
columns: int,
count: int,
margin: tuple[int, int, int, int] = (0, 0, 0, 0),
hit_box_algorithm: HitBoxAlgorithm | None = None,
) -> list[Texture]:
"""
Slice a grid of textures from the sprite sheet.
Args:
size:
Size of each texture ``(width, height)``
columns:
Number of columns in the grid
count:
Number of textures to crop
margin:
The margin around each texture ``(left, right, bottom, top)``
hit_box_algorithm:
Hit box algorithm to use for the textures.
If not provided, the default hit box algorithm will be used.
"""
textures = []
width, height = size
left, right, bottom, top = margin
for sprite_no in range(count):
row = sprite_no // columns
column = sprite_no % columns
x = (width + left + right) * column
y = (height + top + bottom) * row
im = self.image.crop((x, y, x + width, y + height))
texture = Texture(im, hit_box_algorithm=hit_box_algorithm)
texture.file_path = self._path
texture.crop_values = x, y, width, height
textures.append(texture)
return textures