Skip to content

Commit

Permalink
Небольшая оптимизация текстур
Browse files Browse the repository at this point in the history
  • Loading branch information
themanyfaceddemon committed Jul 28, 2024
1 parent f97345c commit 09d9e46
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 60 deletions.
7 changes: 4 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ venv.bak/
.vs/*
.vscode/*

# Data
[Dd]ata/*
# Server data
Content/Servers/*

# Texture
Sprites/
Content/Compiled/*
*_compiled*.gif
*_compiled*.png

# PyQT6
*_ui.py
3 changes: 3 additions & 0 deletions Code/systems/texture_system/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
from systems.texture_system.color import Color
from systems.texture_system.texture_system import TextureSystem

__all__ = ['Color', 'TextureSystem']
58 changes: 58 additions & 0 deletions Code/systems/texture_system/color.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
class Color:
__slots__ = ['_r', '_g', '_b', '_a']

def __init__(self, r: int, g: int, b: int, a: int) -> None:
self.r = r
self.g = g
self.b = b
self.a = a

@property
def r(self) -> int:
return self._r

@r.setter
def r(self, value: int) -> None:
if not (0 <= value <= 255):
raise ValueError("Invalid value for r. It must be between 0 and 255")

self._r = value

@property
def g(self) -> int:
return self._g

@g.setter
def g(self, value: int) -> None:
if not (0 <= value <= 255):
raise ValueError("Invalid value for g. It must be between 0 and 255")

self._g = value

@property
def b(self) -> int:
return self._b

@b.setter
def b(self, value: int) -> None:
if not (0 <= value <= 255):
raise ValueError("Invalid value for b. It must be between 0 and 255")

self._b = value

@property
def a(self) -> int:
return self._a

@a.setter
def a(self, value: int) -> None:
if not (0 <= value <= 255):
raise ValueError("Invalid value for a. It must be between 0 and 255")

self._a = value

def __str__(self) -> str:
return f"{self.r}_{self.g}_{self.b}_{self.a}"

def __repr__(self) -> str:
return f"Color(r={self.r}, g={self.g}, b={self.b}, a={self.a})"
54 changes: 15 additions & 39 deletions Code/systems/texture_system/texture_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@

import yaml
from PIL import Image, ImageSequence
from root_path import ROOT_PATH
from systems.texture_system.color import Color


class TextureSystem:
"""Статический класс TextureSystem отвечает за управление текстурами, включая их загрузку, изменение цвета, и объединение слоев в одно изображение или GIF.
"""
__slots__ = []
DEFAULT_FPS: int = 24
DEFAULT_COLOR: Tuple[int, int, int, int] = (255, 255, 255, 255)
DEFAULT_COLOR: Color = Color(255, 255, 255, 255)

@staticmethod
def _get_hash_list(layers: List[Dict[str, Any]]) -> str:
Expand Down Expand Up @@ -54,32 +56,6 @@ def _slice_image(image: Image.Image, frame_width: int, frame_height: int, num_fr

return frames

@staticmethod
def _get_color_str(color: Tuple[int, int, int, int]) -> str:
"""Возвращает строковое представление цвета.
Args:
color (Tuple[int, int, int, int]): Цвет в формате RGBA.
Returns:
str: Строковое представление цвета.
"""
TextureSystem._validate_color(color)
return '_'.join(map(str, color))

@staticmethod
def _validate_color(color: Tuple[int, int, int, int]) -> None:
"""Проверяет, что цвет в формате RGBA имеет корректные значения.
Args:
color (Tuple[int, int, int, int]): Цвет в формате RGBA.
Raises:
ValueError: Если значения цвета находятся вне диапазона 0-255.
"""
if not all(0 <= c <= 255 for c in color):
raise ValueError("Invalid RGBA color format for texture. All values must be between 0 и 255")

@staticmethod
def get_textures(path: str) -> List[Dict[str, Any]]:
"""Загружает текстуры из указанного пути.
Expand All @@ -93,7 +69,7 @@ def get_textures(path: str) -> List[Dict[str, Any]]:
with open(f"{path}/info.yml", 'r') as file:
info = yaml.safe_load(file)

return info.get('Sprites', [])
return info.get('Texture', [])

@staticmethod
def get_state_info(path: str, state: str) -> Tuple[int, int, int, bool]:
Expand All @@ -112,7 +88,7 @@ def get_state_info(path: str, state: str) -> Tuple[int, int, int, bool]:
with open(f"{path}/info.yml", 'r') as file:
info = yaml.safe_load(file)

info = info.get('Sprites', [])
info = info.get('Texture', [])

sprite_info = next((sprite for sprite in info if sprite['name'] == state), None)
if not sprite_info:
Expand All @@ -126,21 +102,21 @@ def get_state_info(path: str, state: str) -> Tuple[int, int, int, bool]:
return frame_width, frame_height, num_frames, is_mask

@staticmethod
def _get_compiled(path: str, state: str, color: Optional[Tuple[int, int, int, int]] = None, is_gif: bool = False) -> Union[Image.Image, List[Image.Image], None]:
def _get_compiled(path: str, state: str, color: Optional[Color] = None, is_gif: bool = False) -> Union[Image.Image, List[Image.Image], None]:
"""Проверяет наличие компилированного изображения или GIF.
Args:
path (str): Путь к файлу.
state (str): Имя состояния.
color (Optional[Tuple[int, int, int, int]], optional): Цвет в формате RGBA. По умолчанию None.
color (Optional[Color], optional): Цвет в формате RGBA. По умолчанию None.
is_gif (bool, optional): Указывает, является ли изображение GIF. По умолчанию False.
Returns:
Union[Image.Image, List[Image.Image], None]: Изображение или список кадров, если существует, иначе None.
"""
image_path: str = f"{path}/{state}"
if color:
image_path += f"_compiled_{TextureSystem._get_color_str(color)}"
image_path += f"_compiled_{color}"

image_path += ".gif" if is_gif else ".png"

Expand All @@ -155,13 +131,13 @@ def _get_compiled(path: str, state: str, color: Optional[Tuple[int, int, int, in
return None

@staticmethod
def get_image_recolor(path: str, state: str, color: Tuple[int, int, int, int] = DEFAULT_COLOR) -> Image.Image:
def get_image_recolor(path: str, state: str, color: Color = DEFAULT_COLOR) -> Image.Image:
"""Возвращает перекрашенное изображение указанного состояния.
Args:
path (str): Путь к файлу.
state (str): Имя состояния.
color (Tuple[int, int, int, int], optional): Цвет в формате RGBA. По умолчанию DEFAULT_COLOR.
color (Color, optional): Цвет в формате RGBA. По умолчанию DEFAULT_COLOR.
Returns:
Image.Image: Перекрашенное изображение.
Expand All @@ -183,7 +159,7 @@ def get_image_recolor(path: str, state: str, color: Tuple[int, int, int, int] =
]

image.putdata(new_colored_image)
image.save(f"{path}/{state}_compiled_{TextureSystem._get_color_str(color)}.png")
image.save(f"{path}/{state}_compiled_{color}.png")
return image

@staticmethod
Expand All @@ -207,13 +183,13 @@ def get_image(path: str, state: str) -> Image.Image:
raise FileNotFoundError(f"Image file for state '{state}' not found in path '{path}'.")

@staticmethod
def get_gif_recolor(path: str, state: str, color: Tuple[int, int, int, int] = DEFAULT_COLOR, fps: int = DEFAULT_FPS) -> List[Image.Image]:
def get_gif_recolor(path: str, state: str, color: Color = DEFAULT_COLOR, fps: int = DEFAULT_FPS) -> List[Image.Image]:
"""Возвращает перекрашенный GIF указанного состояния.
Args:
path (str): Путь к файлу.
state (str): Имя состояния.
color (Tuple[int, int, int, int], optional): Цвет в формате RGBA. По умолчанию DEFAULT_COLOR.
color (Color, optional): Цвет в формате RGBA. По умолчанию DEFAULT_COLOR.
fps (int, optional): Частота кадров. По умолчанию DEFAULT_FPS.
Returns:
Expand All @@ -229,7 +205,7 @@ def get_gif_recolor(path: str, state: str, color: Tuple[int, int, int, int] = DE

frames = TextureSystem._slice_image(image, frame_width, frame_height, num_frames)

output_path = f"{path}/{state}_compiled_{TextureSystem._get_color_str(color)}.gif"
output_path = f"{path}/{state}_compiled_{color}.gif"
frames[0].save(output_path, save_all=True, append_images=frames[1:], duration=1000//fps, loop=0)

return frames
Expand Down Expand Up @@ -289,7 +265,7 @@ def merge_layers(layers: List[Dict[str, Any]], fps: int = DEFAULT_FPS) -> Union[
Returns:
Union[Image.Image, List[Image.Image]]: Объединенное изображение или список кадров GIF.
"""
base_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', 'Sprites', 'compiled'))
base_path = os.path.join(ROOT_PATH, 'Content', 'Compiled')
if not os.path.exists(base_path):
os.makedirs(base_path)

Expand Down
20 changes: 2 additions & 18 deletions Tests/Texture/TextureSystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@

class TestTextureSystem(unittest.TestCase):
def setUp(self):
self.test_dir = 'test_sprites'
self.test_dir = 'test_texture'
os.makedirs(self.test_dir, exist_ok=True)
self.compiled_dir = os.path.abspath(os.path.join(self.test_dir, 'compiled'))
os.makedirs(self.compiled_dir, exist_ok=True)

info_data = {
'Sprites': [
'Texture': [
{
'name': 'state1',
'size': {'x': 100, 'y': 100},
Expand Down Expand Up @@ -68,22 +68,6 @@ def test_get_hash_list(self):
expected_hash = hashlib.sha256(pickle.dumps(layers)).hexdigest()
self.assertEqual(TextureSystem._get_hash_list(layers), expected_hash)

def test_get_color_str(self):
color = (255, 128, 64, 32)
expected_str = '255_128_64_32'
self.assertEqual(TextureSystem._get_color_str(color), expected_str)

def test_validate_color(self):
valid_color = (255, 128, 64, 32)
try:
TextureSystem._validate_color(valid_color)
except ValueError:
self.fail("_validate_color raised ValueError unexpectedly!")

invalid_color = (256, 128, 64, 32)
with self.assertRaises(ValueError):
TextureSystem._validate_color(invalid_color)

def test_slice_image(self):
image = Image.new('RGBA', (450, 150), 'white')
frames = TextureSystem._slice_image(image, 150, 150, 3)
Expand Down

0 comments on commit 09d9e46

Please sign in to comment.