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

1# Copyright (c) 2026 Shaurya Sharma 

2# SPDX-License-Identifier: MIT 

3 

4import pygame as pg 

5 

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. 

16  

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. 

23 

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 

37 

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. 

46  

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. 

52 

53 Returns:  

54 list[list[pygame.Surface]]: The list of animations. 

55 """ 

56 sheet = pg.image.load(filename).convert_alpha() 

57 all_rows = [] 

58 

59 cols = sheet.get_width() // frame_width 

60 rows = sheet.get_height() // frame_height 

61 

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) 

74 

75 all_rows.append(current_row_frames) # Add the finished row to the master list 

76 

77 return all_rows 

78 

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. 

86  

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. 

91 

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 

105 

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. 

113  

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. 

118 

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 

126 

127 @staticmethod 

128 def make_img_white(surface: pg.Surface, amount: int=255) -> pg.Surface: 

129 """Whiten a surface to a given degree. 

130  

131 Args: 

132 surface (pygame.Surface): The surface to modify. 

133 amount (int, optional): The degree to whiten by. Defaults to 255. 

134 

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 

141 

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. 

151  

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 

164 

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)) 

172 

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. 

180  

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)) 

198 

199 combined_surface.blit(surface, (thickness, thickness)) 

200 return combined_surface