Coverage for topdownengine/visual_utils.py: 21%
78 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-24 02:27 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-24 02:27 +0000
1# Copyright (c) 2026 Shaurya Sharma
2# SPDX-License-Identifier: MIT
4import pygame as pg
6class VisualUtils:
7 @staticmethod
8 def load_animation(
9 filename: str,
10 frame_width: int,
11 frame_height: int,
12 new_frame_width: int=None,
13 new_frame_height: int=None
14 ) -> list[pg.Surface]:
15 """Loads an animation from a given file with a given frame width and height.
17 Args:
18 filename (str): File path.
19 frame_width (int): Frame width.
20 frame_height (int): Frame height.
21 new_frame_width (int, optional): New frame width. Defaults to None.
22 new_frame_height (int, optional): New frame height. Defaults to None.
24 Returns:
25 list[pygame.Surface]: The animation.
26 """
27 sheet = pg.image.load(filename).convert_alpha()
28 frames = []
29 frame_count = sheet.get_width() // frame_width
30 for i in range(frame_count):
31 rect = pg.Rect(i * frame_width, 0, frame_width, frame_height)
32 frame = pg.transform.scale(sheet.subsurface(rect), (frame_width, frame_height))
33 if new_frame_width is not None or new_frame_height is not None:
34 frame = pg.transform.scale(frame, (new_frame_width if new_frame_width is not None else frame_width, new_frame_height if new_frame_height is not None else frame_height))
35 frames.append(frame)
36 return frames
38 @staticmethod
39 def load_animations(
40 filename: str,
41 frame_width: int,
42 frame_height: int,
43 scale_to: int|tuple=None
44 ) -> list[list[pg.Surface]]:
45 """Loads multiple animations from a given spritesheet with a given frame width and height.
47 Args:
48 filename (str): File path.
49 frame_width (int): Frame width.
50 frame_height (int): Frame height.
51 scale_to (int|tuple, optional): Integer/tuple to scale by/to. Defaults to None.
53 Returns:
54 list[list[pygame.Surface]]: The list of animations.
55 """
56 sheet = pg.image.load(filename).convert_alpha()
57 all_rows = []
59 cols = sheet.get_width() // frame_width
60 rows = sheet.get_height() // frame_height
62 for row in range(rows):
63 current_row_frames = [] # Start a new list for this specific row
64 for col in range(cols):
65 rect = pg.Rect(col * frame_width, row * frame_height, frame_width, frame_height)
66 if scale_to:
67 if type(scale_to) == int:
68 frame = pg.transform.scale(sheet.subsurface(rect), (rect.width * scale_to, rect.height * scale_to))
69 else:
70 frame = pg.transform.scale(sheet.subsurface(rect), scale_to)
71 else:
72 frame = sheet.subsurface(rect)
73 current_row_frames.append(frame)
75 all_rows.append(current_row_frames) # Add the finished row to the master list
77 return all_rows
79 @staticmethod
80 def flip_animation(
81 anim: list[pg.Surface],
82 flip_x: bool=False,
83 flip_y: bool=False
84 ) -> list[pg.Surface]:
85 """Flip an animation.
87 Args:
88 anim (list[pygame.Surface]): The animation to flip.
89 flip_x (bool, optional): Flip x? Defaults to False.
90 flip_y (bool, optional): Flip y? Defaults to False.
92 Returns:
93 list[pygame.Surface]: The animation.
94 """
95 new_anim = []
96 for frame in anim:
97 new_anim.append(
98 pg.transform.flip(
99 frame,
100 flip_x,
101 flip_y
102 )
103 )
104 return new_anim
106 @staticmethod
107 def replace_color(
108 surface: pg.Surface,
109 old_color: pg.typing.ColorLike,
110 new_color: pg.typing.ColorLike
111 ) -> pg.Surface:
112 """Replace all of one given color in a Surface with another.
114 Args:
115 surface (pygame.Surface): The surface to modify.
116 old_color (pygame.typing.ColorLike): The old color to replace.
117 new_color (pygame.typing.ColorLike): The new color to replace with.
119 Returns:
120 pygame.Surface: The new Surface.
121 """
122 surface_new = surface.copy()
123 with pg.PixelArray(surface_new) as pixels:
124 pixels.replace(old_color, new_color)
125 return surface_new
127 @staticmethod
128 def make_img_white(surface: pg.Surface, amount: int=255) -> pg.Surface:
129 """Whiten a surface to a given degree.
131 Args:
132 surface (pygame.Surface): The surface to modify.
133 amount (int, optional): The degree to whiten by. Defaults to 255.
135 Returns:
136 pygame.Surface: The new Surface.
137 """
138 silhouette = surface.copy()
139 silhouette.fill((amount, amount, amount, 0), special_flags=pg.BLEND_RGBA_ADD)
140 return silhouette
142 @staticmethod
143 def draw_low_res_line(
144 surface: pg.Surface,
145 color: pg.typing.ColorLike,
146 start_pos: pg.typing.Point,
147 end_pos: pg.typing.Point,
148 res: int=256
149 ) -> None:
150 """Draw a line that matches the game's pixel resolution.
152 Args:
153 surface (pygame.Surface): The surface to draw the line on.
154 color (pygame.typing.ColorLike): The color of the line.
155 start_pos (pygame.typing.Point): The start position.
156 end_pos (pygame.typing.Point): The end position.
157 res (int, optional): The amount to divide by. Defaults to 256.
158 """
159 sw, sh = surface.get_size()
160 if sw > sh:
161 lw, lh = res, int(res * (sh / sw))
162 else:
163 lw, lh = int(res * (sw / sh)), res
165 low_res_surf = pg.Surface((max(1, lw), max(1, lh)), pg.SRCALPHA)
166 scale = sw / lw
167 x0, y0 = int(start_pos[0] / scale), int(start_pos[1] / scale)
168 x1, y1 = int(end_pos[0] / scale), int(end_pos[1] / scale)
169 pg.draw.line(low_res_surf, color, (x0, y0), (x1, y1), 1)
170 final_surf = pg.transform.scale(low_res_surf, (sw, sh))
171 surface.blit(final_surf, (0, 0))
173 @staticmethod
174 def create_outline(
175 surface: pg.Surface,
176 thickness: int,
177 outline_color: pg.typing.ColorLike
178 ) -> pg.Surface:
179 """Create an outline of a given thickness and color on a Surface.
181 Args:
182 surface (pygame.Surface): The surface to outline.
183 thickness (int): The thickness of the outline.
184 outline_color (pygame.typing.ColorLike): The color of the outline.
185 """
186 mask = pg.mask.from_surface(surface)
187 mask_surface = mask.to_surface(setcolor=outline_color)
188 mask_surface.set_colorkey((0, 0, 0))
189 new_width = surface.get_width() + (thickness * 2)
190 new_height = surface.get_height() + (thickness * 2)
191 combined_surface = pg.Surface((new_width, new_height), pg.SRCALPHA)
192 for dx in range(-thickness, thickness + 1):
193 for dy in range(-thickness, thickness + 1):
194 if dx == 0 and dy == 0:
195 continue
196 if dx**2 + dy**2 <= thickness**2:
197 combined_surface.blit(mask_surface, (dx + thickness, dy + thickness))
199 combined_surface.blit(surface, (thickness, thickness))
200 return combined_surface