Skip to content

Commit

Permalink
Rectify spritesheet (#2481)
Browse files Browse the repository at this point in the history
* rectify spritesheet

* the ghost of henry ford haunts me in my IDE

* typo

* rectify load_or_get_spritesheet_texture

* re-add y-down support

* I will paint a Model T fluorescent orange out of spite

* fix typing
  • Loading branch information
DigiDuncan authored Jan 11, 2025
1 parent 0d0ee61 commit 5e800f7
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 68 deletions.
12 changes: 5 additions & 7 deletions arcade/texture/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
TextureCache,
)
from arcade.texture import ImageData, SpriteSheet
from arcade.types.rect import Rect

from .texture import Texture

Expand Down Expand Up @@ -128,10 +129,7 @@ def load_or_get_spritesheet(self, path: str | Path) -> SpriteSheet:
def load_or_get_spritesheet_texture(
self,
path: str | Path,
x: int,
y: int,
width: int,
height: int,
rect: Rect,
hit_box_algorithm: hitbox.HitBoxAlgorithm | None = None,
) -> Texture:
"""
Expand All @@ -155,22 +153,22 @@ def load_or_get_spritesheet_texture(
Hit box algorithm to use. If not specified, the global default will be used.
"""
real_path = self._get_real_path(path)
texture = self._texture_cache.get_texture_by_filepath(real_path, crop=(x, y, width, height))
texture = self._texture_cache.get_texture_by_filepath(real_path, crop=rect.lbwh_int)
if texture:
return texture

# check if sprite sheet is cached and load if not
sprite_sheet = self.load_or_get_spritesheet(real_path)

# slice out the texture and cache + return
texture = sprite_sheet.get_texture(x, y, width, height, hit_box_algorithm=hit_box_algorithm)
texture = sprite_sheet.get_texture(rect, hit_box_algorithm=hit_box_algorithm)
self._texture_cache.put(texture)
if texture.image_cache_name:
self._image_data_cache.put(texture.image_cache_name, texture.image_data)

# Add to image data cache
self._image_data_cache.put(
Texture.create_image_cache_name(real_path, (x, y, width, height)),
Texture.create_image_cache_name(real_path, rect.lbwh_int),
texture.image_data,
)

Expand Down
66 changes: 27 additions & 39 deletions arcade/texture/spritesheet.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

# from arcade import Texture
from arcade.texture import Texture
from arcade.types.rect import Rect

if TYPE_CHECKING:
from arcade.hitbox import HitBoxAlgorithm
Expand Down Expand Up @@ -102,68 +103,55 @@ def flip_top_bottom(self) -> None:
self._image = self._image.transpose(Transpose.FLIP_TOP_BOTTOM)
self._flip_flags = (self._flip_flags[0], not self._flip_flags[1])

def get_image(
self, x: int, y: int, width: int, height: int, origin: OriginChoices = "upper_left"
) -> Image.Image:
def get_image(self, rect: Rect, y_up=False) -> Image.Image:
"""
Slice out an image from the sprite sheet.
Args:
x:
X position of the image
y:
Y position of the image
width:
Width of the image.
height:
Height of the image.
origin:
Origin of the image. Default is "upper_left".
Options are "upper_left" or "lower_left".
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 origin == "upper_left":
return self.image.crop((x, y, x + width, y + height))
elif origin == "lower_left":
if y_up:
return self.image.crop(
(x, self.image.height - y - height, x + width, self.image.height - y)
(
rect.left,
self.image.height - rect.bottom - rect.height,
rect.right,
self.image.height - rect.bottom,
)
)
else:
raise ValueError("Invalid value for origin. Must be 'upper_left' or 'lower_left'.")
return self.image.crop(
(
rect.left,
rect.bottom,
rect.right,
rect.top,
)
)

# slice an image out of the sprite sheet
def get_texture(
self,
x: int,
y: int,
width: int,
height: int,
hit_box_algorithm: HitBoxAlgorithm | None = None,
origin: OriginChoices = "upper_left",
self, rect: Rect, hit_box_algorithm: HitBoxAlgorithm | None = None, y_up=False
) -> Texture:
"""
Slice out texture from the sprite sheet.
Args:
x:
X position of the texture (lower left corner).
y:
Y position of the texture (lower left corner).
width:
Width of the texture.
height:
Height of the texture.
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.
origin:
Origin of the texture. Default is "upper_left".
Options are "upper_left" or "lower_left".
"""
im = self.get_image(x, y, width, height, origin=origin)
im = self.get_image(rect, y_up)
texture = Texture(im, hit_box_algorithm=hit_box_algorithm)
texture.file_path = self._path
texture.crop_values = x, y, width, height
texture.crop_values = rect.lbwh_int
return texture

def get_image_grid(
Expand Down
11 changes: 6 additions & 5 deletions doc/tutorials/crt_filter/crt_filter_example.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import arcade
from arcade.experimental.crt_filter import CRTFilter
from arcade.types.rect import LBWH
from pyglet.math import Vec2


Expand Down Expand Up @@ -35,11 +36,11 @@ def __init__(self, width, height, title):

# Slice out some textures from the sprite sheet
spritesheet = arcade.load_spritesheet("pac_man_sprite_sheet.png")
ghost_red = spritesheet.get_texture(x=4, y=65, width=13, height=15)
pink_ghost = spritesheet.get_texture(x=4, y=81, width=13, height=15)
pacman_1 = spritesheet.get_texture(x=4, y=1, width=13, height=15)
pacman_2 = spritesheet.get_texture(x=20, y=1, width=13, height=15)
pacman_3 = spritesheet.get_texture(x=36, y=1, width=13, height=15)
ghost_red = spritesheet.get_texture(LBWH(4, 65, 13, 15))
pink_ghost = spritesheet.get_texture(LBWH(4, 81, 13, 15))
pacman_1 = spritesheet.get_texture(LBWH(4, 1, 13, 15))
pacman_2 = spritesheet.get_texture(LBWH(20, 1, 13, 15))
pacman_3 = spritesheet.get_texture(LBWH(36, 1, 13, 15))

# Create sprite for the red ghost with some movement and add it to the sprite list
sprite = arcade.Sprite(ghost_red, center_x=100, center_y=300, scale=5.0)
Expand Down
9 changes: 5 additions & 4 deletions tests/unit/texture/test_manager.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Test the TextureCacheManager"""
import arcade
from arcade.types.rect import LBWH

SPRITESHEET_PATH = ":assets:images/spritesheets/codepage_437.png"
TEST_TEXTURE = ":assets:images/test_textures/test_texture.png"
Expand All @@ -23,17 +24,17 @@ def test_load_spritesheet():
def test_load_spritesheet_texture():
"""Load spritesheet and test caching"""
manager = arcade.texture.TextureCacheManager()
texture = manager.load_or_get_spritesheet_texture(SPRITESHEET_PATH, 0, 0, 8, 16)
texture = manager.load_or_get_spritesheet_texture(SPRITESHEET_PATH, LBWH(0, 0, 8, 16))
# This should have cached the spritesheet
assert len(manager._sprite_sheets) == 1
assert isinstance(list(manager._sprite_sheets.values())[0], arcade.SpriteSheet)
# The same texture should be returned the second time
assert manager.load_or_get_spritesheet_texture(SPRITESHEET_PATH, 0, 0, 8, 16) == texture
assert manager.load_or_get_spritesheet_texture(SPRITESHEET_PATH, LBWH(0, 0, 8, 16)) == texture

# Load a few more textures
for i in range(10):
texture = manager.load_or_get_spritesheet_texture(SPRITESHEET_PATH, i * 9, 0, 8, 16)
assert manager.load_or_get_spritesheet_texture(SPRITESHEET_PATH, i * 9, 0, 8, 16) == texture
texture = manager.load_or_get_spritesheet_texture(SPRITESHEET_PATH, LBWH(i * 9, 0, 8, 16))
assert manager.load_or_get_spritesheet_texture(SPRITESHEET_PATH, LBWH(i * 9, 0, 8, 16)) == texture

# We should still have 1 spritesheet
assert len(manager._sprite_sheets) == 1
Expand Down
21 changes: 8 additions & 13 deletions tests/unit/texture/test_sprite_sheet.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from arcade.types.rect import LBWH
import pytest
from PIL import Image
import arcade
Expand Down Expand Up @@ -74,32 +75,26 @@ def test_get_image(sprite_sheet):

# Crop out the dollar sign using upper left origin
im = sprite_sheet.get_image(
x=9 * 4, # 4th column
y=16, # second row
width=8,
height=16,
origin="upper_left",
)
LBWH(9 * 4, # 4th column
16, # second row
8, 16))
assert isinstance(im, Image.Image)
assert im.size == (8, 16)
assert im.tobytes() == dollar_sign.tobytes()

# Crop out the dollar sign using lower left origin
im = sprite_sheet.get_image(
x=9 * 4, # 4th column
y=16 * 6, # 6th row
width=8,
height=16,
origin="lower_left",
)
LBWH(9 * 4, # 4th column
16 * 6, # 6th row
8,16), True)
assert isinstance(im, Image.Image)
assert im.size == (8, 16)
assert im.tobytes() == dollar_sign.tobytes()


def test_get_texture(sprite_sheet):
"""Get a texture from the sprite sheet."""
texture = sprite_sheet.get_texture(0, 0, 8, 16)
texture = sprite_sheet.get_texture(LBWH(0, 0, 8, 16))
assert isinstance(texture, arcade.Texture)
assert texture.image.size == (8, 16)

Expand Down

0 comments on commit 5e800f7

Please sign in to comment.