Coverage for src/ensae_teaching_cs/helpers/pygame_helper.py: 79%
147 statements
« prev ^ index » next coverage.py v7.1.0, created at 2023-04-28 06:23 +0200
« prev ^ index » next coverage.py v7.1.0, created at 2023-04-28 06:23 +0200
1"""
2@file
3@brief pygame helpers
5The module pygame is not imported in this module but sent
6to every function as a parameter to avoid importing
7the module if not needed.
8"""
9import math
12MOUSE = "mouse"
13KEY = "key"
16def wait_event(pygame):
17 """
18 The function waits for an event, a
20 @param pygame module pygame
21 """
22 while True:
23 for event in pygame.event.get():
24 if event.type == pygame.MOUSEBUTTONUP:
25 return (MOUSE, event.button, event.pos[0], event.pos[1])
26 elif event.type == pygame.KEYUP:
27 if event.key == 27:
28 return None
29 else:
30 return (KEY, event.key)
31 elif event.type == pygame.QUIT:
32 return None
35def empty_main_loop(pygame, msg=None):
36 """
37 Removes all events in the main loop,
38 a mouse click make the program halt,
39 another click makes it start again.
41 @param pygame module pygame
42 @return event ``pygame.QUIT``?
43 """
44 for event in pygame.event.get():
45 if event.type == pygame.QUIT:
46 return False
47 if event.type == pygame.MOUSEBUTTONUP:
48 if msg is not None:
49 print(msg())
50 wait_event(pygame)
51 return True
54def get_pygame_screen_font(h, size, flags=0):
55 """
56 Creates a surface with :epkg:`pygame`, initialize the module,
57 creates font.
59 @param h size of the main font
60 @param size screen size
61 @param flags see `pygame.display.set_mode <https://www.pygame.org/docs/ref/display.html#pygame.display.set_mode>`_
62 @return pygame, screen, dictionary of fonts
64 The dictionary of fonts contains three fonts of size *h*,
65 *3h/4*, *5h/6*.
67 This function leaves file still opened and generates warnings.
68 Parameter *flag* can be useful if you run the function just
69 to test that it is working and the result does not need to be seen.
70 """
71 import pygame
72 pygame.init()
73 font = pygame.font.Font("freesansbold.ttf", h)
74 font_small = pygame.font.Font("freesansbold.ttf", 3 * h // 4)
75 try:
76 screen = pygame.display.set_mode(size, flags)
77 except pygame.error as e:
78 raise RuntimeError(
79 f"Unable to create a screen, flags={flags}") from e
80 font = pygame.font.Font("freesansbold.ttf", h)
81 font_small = pygame.font.Font("freesansbold.ttf", 3 * h // 4)
82 font_half = pygame.font.Font("freesansbold.ttf", 5 * h // 6)
83 return pygame, screen, dict(font=font, font_half=font_half, font_small=font_small)
86def build_diff_image(pygame, screen, h, maxw, seq1=None, seq2=None, diff=None, fonts=None,
87 bars=None, colors=None, progress=None, prev_bars=None):
88 """
89 Builds an image to show a difference between two lists,
90 we assume these list contain distinct items.
92 @param pygame module pygame
93 @param screen screen (pygame surface)
94 @param h height of a line
95 @param maxw width of the screen
96 @param seq1 list 1 (can be None)
97 @param seq2 list 2 (cannot be None)
98 @param diff difference (object `SequenceMatcher <https://docs.python.org/3.5/library/difflib.html#sequencematcher-objects>`_)
99 @param fonts dictionary of fonts with keys ``'font'``, ``'font_small'``, ``'font_half'``
100 @param bars each item of sequence 2 can be associated to a width (in [0, 1])
101 @param colors dictionary of colors (see below)
102 @param progress draws the progress between two list
103 @param prev_bars previous width
105 Colors:
107 * black: no change
108 * blue: new
109 * red: deleted
110 * green: vert
111 * yellow: bars
113 When *progress* is not None, the picture is a kind of average
114 between the previous position and the new one. When a suggestion moves
115 from *p1* to *p2*, it defines a circle.
116 The result looks like this.
118 .. raw:: html
120 <video autoplay=" controls="" loop="" height="250">
121 <source src="http://www.xavierdupre.fr/enseignement/complements/diff.mp4" type="video/mp4" />
122 </video>
124 """
125 font = fonts.get('font', None)
126 font_small = fonts.get('font_small', None)
127 font_half = fonts.get('font_half', None)
128 if font is None:
129 raise ValueError("font cannot be None")
130 if font_small is None:
131 raise ValueError("font_small cannot be None")
132 if font_half is None:
133 raise ValueError("font_half cannot be None")
134 if seq2 is None:
135 raise ValueError("seq2 cannot be None")
137 if colors is None:
138 colors = {}
139 set_seq1 = {} if seq1 is None else set(seq1)
140 set_seq2 = set(seq2)
141 width = h // 3
142 color_bar = colors.get('yellow', (240, 240, 0))
143 pos = 0
144 if diff is not None:
145 if progress is None:
146 # just the diff
147 opcodes = []
148 for opcode in diff.get_opcodes():
149 if opcode[0] in {'delete', 'equal', 'insert'}:
150 opcodes.append(opcode)
151 elif opcode[0] == "replace":
152 opcodes.append(
153 ('delete', opcode[1], opcode[2], None, None))
154 opcodes.append(
155 ('insert', None, None, opcode[3], opcode[4]))
156 else:
157 raise ValueError(f"unexpected: {opcode}")
159 for opcode in opcodes:
160 if opcode[0] == "delete":
161 for i in range(opcode[1], opcode[2]):
162 text = seq1[i]
163 if text not in set_seq2:
164 color = colors.get('red', (200, 0, 0))
165 text = font_small.render(text, True, color)
166 screen.blit(text, (10, h * pos + h // 6))
167 pos += 1
168 else:
169 # we skip, it is going to be display by the other
170 # part of the loop
171 pass
172 elif opcode[0] == "equal":
173 color = colors.get('black', (0, 0, 0))
174 for i in range(opcode[3], opcode[4]):
175 if bars is not None:
176 y = h * pos + (h - width) // 2 + width
177 pygame.draw.line(
178 screen, color_bar, (0, y), (int(bars[i] * maxw), y), width)
179 text = seq2[i]
180 text = font.render(text, True, color)
181 screen.blit(text, (10, h * pos))
182 pos += 1
183 else:
184 for i in range(opcode[3], opcode[4]):
185 if bars is not None:
186 y = h * pos + (h - width) // 2 + width
187 pygame.draw.line(
188 screen, color_bar, (0, y), (int(bars[i] * maxw), y), width)
189 text = seq2[i]
190 if text in set_seq1:
191 color = colors.get('green', (0, 120, 0))
192 text = font.render(text, True, color)
193 screen.blit(text, (10, h * pos))
194 pos += 1
195 else:
196 color = colors.get("blue", (0, 120, 120))
197 text = font.render(text, True, color)
198 screen.blit(text, (10, h * pos))
199 pos += 1
200 else:
201 # animation
202 positions = []
203 opcodes = []
204 for opcode in diff.get_opcodes():
205 if opcode[0] in {'delete', 'equal', 'insert'}:
206 opcodes.append(opcode)
207 elif opcode[0] == "replace":
208 opcodes.append(
209 ('delete', opcode[1], opcode[2], None, None))
210 opcodes.append(
211 ('insert', None, None, opcode[3], opcode[4]))
212 else:
213 raise ValueError(f"unexpected: {opcode}")
215 for opcode in opcodes:
216 if opcode[0] == "delete":
217 for i in range(opcode[1], opcode[2]):
218 row = (seq1[i], i, seq2.index(seq1[i]) if seq1[i] in seq2 else None,
219 prev_bars[i] if prev_bars is not None else None)
220 positions.append(row)
221 elif opcode[0] == "equal":
222 for i in range(opcode[3], opcode[4]):
223 row = (seq2[i], seq1.index(seq2[i]), i,
224 bars[i] if bars is not None else None)
225 positions.append(row)
226 else:
227 for i in range(opcode[3], opcode[4]):
228 row = (seq2[i], seq1.index(seq2[i]) if seq2[i] in seq1 else None, i,
229 bars[i] if bars is not None else None)
230 positions.append(row)
231 for text, p1, p2, bar_ in positions:
232 if p1 is None:
233 # new
234 x = maxw * (1 - progress)
235 y = p2 * h
236 color = colors.get('blue', (0, 120, 120))
237 elif p2 is None:
238 # deleted
239 x = maxw * progress
240 y = p1 * h
241 color = colors.get('green', (0, 120, 0))
242 else:
243 # moved or equal
244 if p1 == p2:
245 x = 0.0
246 y = p1 * h
247 else:
248 x = math.sin(progress * math.pi) * maxw / 2 * \
249 abs(p2 - p1) / len(seq2)
250 y = (p1 + p2) * h / 2 - (p2 - p1) * \
251 h * math.cos(progress * math.pi) / 2
252 color = colors.get('black', (0, 0, 0))
254 x = int(x)
255 y = int(y)
256 if bar_ is not None:
257 y2 = y + (h - width) // 2 + width
258 pygame.draw.line(screen, color_bar, (x, y2),
259 (x + int(bar_ * maxw), y2), width)
260 text = font.render(text, True, color)
261 screen.blit(text, (x, y))
263 else:
264 color = colors.get('black', (0, 0, 0))
265 for i in range(0, len(seq2)):
266 text = seq2[i]
267 text = font.render(text, True, color)
268 screen.blit(text, (10, h * pos))
269 pos += 1