e2D 1.4.22__tar.gz → 1.4.24__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {e2d-1.4.22 → e2d-1.4.24}/PKG-INFO +1 -1
- {e2d-1.4.22 → e2d-1.4.24}/e2D/__init__.py +22 -23
- {e2d-1.4.22 → e2d-1.4.24}/e2D/__init__.pyi +0 -1
- {e2d-1.4.22 → e2d-1.4.24}/e2D/envs.py +30 -22
- {e2d-1.4.22 → e2d-1.4.24}/e2D/utils.py +6 -2
- e2d-1.4.24/e2D/winrec.py +211 -0
- {e2d-1.4.22 → e2d-1.4.24}/e2D.egg-info/PKG-INFO +1 -1
- {e2d-1.4.22 → e2d-1.4.24}/setup.cfg +1 -1
- e2d-1.4.22/e2D/winrec.py +0 -27
- {e2d-1.4.22 → e2d-1.4.24}/LICENSE +0 -0
- {e2d-1.4.22 → e2d-1.4.24}/README.md +0 -0
- {e2d-1.4.22 → e2d-1.4.24}/e2D/colors.py +0 -0
- {e2d-1.4.22 → e2d-1.4.24}/e2D/def_colors.py +0 -0
- {e2d-1.4.22 → e2d-1.4.24}/e2D/plots.py +0 -0
- {e2d-1.4.22 → e2d-1.4.24}/e2D.egg-info/SOURCES.txt +0 -0
- {e2d-1.4.22 → e2d-1.4.24}/e2D.egg-info/dependency_links.txt +0 -0
- {e2d-1.4.22 → e2d-1.4.24}/e2D.egg-info/requires.txt +0 -0
- {e2d-1.4.22 → e2d-1.4.24}/e2D.egg-info/top_level.txt +0 -0
- {e2d-1.4.22 → e2d-1.4.24}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: e2D
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.24
|
|
4
4
|
Summary: Python library for 2D games. Streamlines dev with keyboard/mouse input, vector calculations, color manipulation, and collision detection. Simplify game creation and unleash creativity!
|
|
5
5
|
Home-page: https://github.com/marick-py/e2D
|
|
6
6
|
Author: Riccardo Mariani
|
|
@@ -52,7 +52,6 @@ class Vector2D:
|
|
|
52
52
|
def aspect_y(self, new_aspect) -> None:
|
|
53
53
|
self.y = self.x * new_aspect
|
|
54
54
|
|
|
55
|
-
@property
|
|
56
55
|
def copy(self) -> "Vector2D":
|
|
57
56
|
return Vector2D(self.x, self.y)
|
|
58
57
|
|
|
@@ -70,7 +69,7 @@ class Vector2D:
|
|
|
70
69
|
@property
|
|
71
70
|
def normalized(self) -> "Vector2D":
|
|
72
71
|
if (mag:=self.length) == 0:
|
|
73
|
-
return self.copy
|
|
72
|
+
return self.copy()
|
|
74
73
|
return Vector2D(self.x / mag, self.y / mag)
|
|
75
74
|
|
|
76
75
|
@property
|
|
@@ -471,47 +470,47 @@ class Vector2D:
|
|
|
471
470
|
def down_left_norm(cls) -> "Vector2D": return V2down_left_norm
|
|
472
471
|
|
|
473
472
|
@classmethod
|
|
474
|
-
def new_zero(cls) -> "Vector2D": return V2zero.copy
|
|
473
|
+
def new_zero(cls) -> "Vector2D": return V2zero.copy()
|
|
475
474
|
@classmethod
|
|
476
|
-
def new_one(cls) -> "Vector2D": return V2one.copy
|
|
475
|
+
def new_one(cls) -> "Vector2D": return V2one.copy()
|
|
477
476
|
@classmethod
|
|
478
|
-
def new_two(cls) -> "Vector2D": return V2two.copy
|
|
477
|
+
def new_two(cls) -> "Vector2D": return V2two.copy()
|
|
479
478
|
@classmethod
|
|
480
|
-
def new_pi(cls) -> "Vector2D": return V2pi.copy
|
|
479
|
+
def new_pi(cls) -> "Vector2D": return V2pi.copy()
|
|
481
480
|
@classmethod
|
|
482
|
-
def new_inf(cls) -> "Vector2D": return V2inf.copy
|
|
481
|
+
def new_inf(cls) -> "Vector2D": return V2inf.copy()
|
|
483
482
|
@classmethod
|
|
484
|
-
def new_neg_one(cls) -> "Vector2D": return V2neg_one.copy
|
|
483
|
+
def new_neg_one(cls) -> "Vector2D": return V2neg_one.copy()
|
|
485
484
|
@classmethod
|
|
486
|
-
def new_neg_two(cls) -> "Vector2D": return V2neg_two.copy
|
|
485
|
+
def new_neg_two(cls) -> "Vector2D": return V2neg_two.copy()
|
|
487
486
|
@classmethod
|
|
488
|
-
def new_neg_pi(cls) -> "Vector2D": return V2neg_pi.copy
|
|
487
|
+
def new_neg_pi(cls) -> "Vector2D": return V2neg_pi.copy()
|
|
489
488
|
@classmethod
|
|
490
|
-
def new_neg_inf(cls) -> "Vector2D": return V2neg_inf.copy
|
|
489
|
+
def new_neg_inf(cls) -> "Vector2D": return V2neg_inf.copy()
|
|
491
490
|
@classmethod
|
|
492
|
-
def new_up(cls) -> "Vector2D": return V2up.copy
|
|
491
|
+
def new_up(cls) -> "Vector2D": return V2up.copy()
|
|
493
492
|
@classmethod
|
|
494
|
-
def new_right(cls) -> "Vector2D": return V2right.copy
|
|
493
|
+
def new_right(cls) -> "Vector2D": return V2right.copy()
|
|
495
494
|
@classmethod
|
|
496
|
-
def new_down(cls) -> "Vector2D": return V2down.copy
|
|
495
|
+
def new_down(cls) -> "Vector2D": return V2down.copy()
|
|
497
496
|
@classmethod
|
|
498
|
-
def new_left(cls) -> "Vector2D": return V2left.copy
|
|
497
|
+
def new_left(cls) -> "Vector2D": return V2left.copy()
|
|
499
498
|
@classmethod
|
|
500
|
-
def new_up_right(cls) -> "Vector2D": return V2up_right.copy
|
|
499
|
+
def new_up_right(cls) -> "Vector2D": return V2up_right.copy()
|
|
501
500
|
@classmethod
|
|
502
|
-
def new_down_right(cls) -> "Vector2D": return V2down_right.copy
|
|
501
|
+
def new_down_right(cls) -> "Vector2D": return V2down_right.copy()
|
|
503
502
|
@classmethod
|
|
504
|
-
def new_up_left(cls) -> "Vector2D": return V2up_left.copy
|
|
503
|
+
def new_up_left(cls) -> "Vector2D": return V2up_left.copy()
|
|
505
504
|
@classmethod
|
|
506
|
-
def new_down_left(cls) -> "Vector2D": return V2down_left.copy
|
|
505
|
+
def new_down_left(cls) -> "Vector2D": return V2down_left.copy()
|
|
507
506
|
@classmethod
|
|
508
|
-
def new_up_right_norm(cls) -> "Vector2D": return V2up_right_norm.copy
|
|
507
|
+
def new_up_right_norm(cls) -> "Vector2D": return V2up_right_norm.copy()
|
|
509
508
|
@classmethod
|
|
510
|
-
def new_down_right_norm(cls) -> "Vector2D": return V2down_right_norm.copy
|
|
509
|
+
def new_down_right_norm(cls) -> "Vector2D": return V2down_right_norm.copy()
|
|
511
510
|
@classmethod
|
|
512
|
-
def new_up_left_norm(cls) -> "Vector2D": return V2up_left_norm.copy
|
|
511
|
+
def new_up_left_norm(cls) -> "Vector2D": return V2up_left_norm.copy()
|
|
513
512
|
@classmethod
|
|
514
|
-
def new_down_left_norm(cls) -> "Vector2D": return V2down_left_norm.copy
|
|
513
|
+
def new_down_left_norm(cls) -> "Vector2D": return V2down_left_norm.copy()
|
|
515
514
|
|
|
516
515
|
|
|
517
516
|
V2 = Vector2D
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
from typing import Literal
|
|
2
|
+
from typing import Literal, Optional
|
|
3
3
|
|
|
4
4
|
from .utils import *
|
|
5
5
|
import pygame as pg
|
|
@@ -63,7 +63,10 @@ class RootEnv:
|
|
|
63
63
|
self.show_fps = show_fps
|
|
64
64
|
self.events :list[pg.event.Event]= []
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
# Track start time for accurate runtime calculation (excluding compilation time)
|
|
67
|
+
self.set_starting_timer()
|
|
68
|
+
|
|
69
|
+
self.__background_color__ :pg.Color= BLACK_COLOR_PYG
|
|
67
70
|
|
|
68
71
|
self.clear_screen_each_frame = clear_screen_each_frame
|
|
69
72
|
self.utils :dict[int|str, Util]= {}
|
|
@@ -73,9 +76,12 @@ class RootEnv:
|
|
|
73
76
|
self.fps_label = Label(str(round(self.current_fps,2)), self.screen_size * .01, V2(250, 50), BLACK_COLOR_PYG, TRANSPARENT_COLOR_PYG, WHITE_COLOR_PYG, border_width=0, starting_hidden=(not self.show_fps), pivot_position="top_left", font=FONT_ARIAL_32)
|
|
74
77
|
self.add_utils(self.fps_label)
|
|
75
78
|
|
|
76
|
-
def
|
|
79
|
+
def set_starting_timer(self) -> None:
|
|
80
|
+
self.__start_time__ = pg.time.get_ticks()
|
|
81
|
+
|
|
82
|
+
def init_rec(self, fps:int=30, draw_on_screen:bool=True, path:str='output.mp4', font:pg.font.Font=FONT_MONOSPACE_16) -> None:
|
|
77
83
|
from .winrec import WinRec
|
|
78
|
-
self.__winrecorder__ = WinRec(self, fps=fps, path=path)
|
|
84
|
+
self.__winrecorder__ = WinRec(self, fps=fps, draw_on_screen=draw_on_screen, path=path, font=font)
|
|
79
85
|
|
|
80
86
|
@property
|
|
81
87
|
def background_color(self) -> Color:
|
|
@@ -97,24 +103,24 @@ class RootEnv:
|
|
|
97
103
|
self.screen = pg.display.set_mode(self.__screen_size__(), vsync=self.__vsync__, flags=self.__flags__, display=self.__display_index__)
|
|
98
104
|
|
|
99
105
|
@property
|
|
100
|
-
def delta(self) ->
|
|
106
|
+
def delta(self) -> float:
|
|
101
107
|
return self.__dt__
|
|
102
108
|
|
|
103
109
|
def get_teoric_max_fps(self) -> float:
|
|
104
110
|
rawdelta = self.clock.get_rawtime()
|
|
105
111
|
return (1000 / rawdelta) if rawdelta != 0 else 1
|
|
106
112
|
|
|
107
|
-
def update_screen_mode(self, vsync:
|
|
108
|
-
self.__vsync__ = vsync
|
|
109
|
-
self.__flags__ = flags
|
|
110
|
-
self.__display_index__ = display_index
|
|
113
|
+
def update_screen_mode(self, vsync:Optional[bool]=None, flags:Optional[int]=None, display_index:Optional[int]=None) -> None:
|
|
114
|
+
if vsync is not None: self.__vsync__ = vsync
|
|
115
|
+
if flags is not None: self.__flags__ = flags
|
|
116
|
+
if display_index is not None: self.__display_index__ = display_index
|
|
111
117
|
self.screen = pg.display.set_mode(self.__screen_size__(), vsync=self.__vsync__, flags=self.__flags__, display=self.__display_index__)
|
|
112
118
|
|
|
113
119
|
def sleep(self, seconds:int|float, precise_delay=False) -> None:
|
|
114
120
|
if precise_delay:
|
|
115
|
-
pg.time.delay(seconds * 1000)
|
|
121
|
+
pg.time.delay(int(seconds * 1000))
|
|
116
122
|
else:
|
|
117
|
-
pg.time.wait(seconds * 1000)
|
|
123
|
+
pg.time.wait(int(seconds * 1000))
|
|
118
124
|
|
|
119
125
|
def add_utils(self, *utils:Util) -> None:
|
|
120
126
|
for util in utils:
|
|
@@ -126,16 +132,16 @@ class RootEnv:
|
|
|
126
132
|
|
|
127
133
|
def remove_utils(self, *utils:int|str|Util) -> None:
|
|
128
134
|
for uid in utils:
|
|
129
|
-
if uid
|
|
130
|
-
del self.utils[uid]
|
|
131
|
-
elif isinstance(uid, Util):
|
|
135
|
+
if isinstance(uid, Util):
|
|
132
136
|
del self.utils[uid.id]
|
|
137
|
+
elif uid in self.utils:
|
|
138
|
+
del self.utils[uid]
|
|
133
139
|
else:
|
|
134
140
|
raise Exception(f"Unknown util type: {uid}")
|
|
135
141
|
|
|
136
142
|
def __new_util_id__(self) -> int:
|
|
137
143
|
if not self.utils: return 0
|
|
138
|
-
else: return
|
|
144
|
+
else: return len(self.utils.keys()) + 1
|
|
139
145
|
|
|
140
146
|
def get_util(self, uid:int|str) -> Util|None:
|
|
141
147
|
if isinstance(uid, Util):
|
|
@@ -147,7 +153,7 @@ class RootEnv:
|
|
|
147
153
|
|
|
148
154
|
@property
|
|
149
155
|
def runtime_seconds(self) -> float:
|
|
150
|
-
return pg.time.get_ticks() / 1e3
|
|
156
|
+
return (pg.time.get_ticks() - self.__start_time__) / 1e3
|
|
151
157
|
|
|
152
158
|
def init(self, sub_env:DefEnv) -> None:
|
|
153
159
|
self.env = sub_env
|
|
@@ -170,18 +176,19 @@ class RootEnv:
|
|
|
170
176
|
border_radius : int|list[int]|tuple[int,int,int,int] = -1,
|
|
171
177
|
margin : Vector2D = Vector2D.zero(),
|
|
172
178
|
personalized_surface : pg.Surface|None = None
|
|
173
|
-
) ->
|
|
179
|
+
) -> Vector2D:
|
|
174
180
|
|
|
175
181
|
text_box = font.render(text, True, color)
|
|
176
182
|
size = Vector2D(*text_box.get_size()) + margin * 2
|
|
177
183
|
pivotted_position = position - size * __PIVOT_POSITIONS_MULTIPLIER__[pivot_position] + margin
|
|
178
|
-
if not any(isinstance(border_radius, cls) for cls in {tuple, list}): border_radius = [border_radius]*4
|
|
184
|
+
if not any(isinstance(border_radius, cls) for cls in {tuple, list}): border_radius = [border_radius] * 4
|
|
179
185
|
surface = (self.screen if personalized_surface == None else personalized_surface)
|
|
180
186
|
if bg_color != None:
|
|
181
187
|
pg.draw.rect(surface, bg_color, (pivotted_position - margin)() + size(), 0, -1, *border_radius)
|
|
182
188
|
if border_width:
|
|
183
189
|
pg.draw.rect(surface, border_color, (pivotted_position - margin)() + size(), border_width, -1, *border_radius)
|
|
184
190
|
surface.blit(text_box, pivotted_position())
|
|
191
|
+
return size
|
|
185
192
|
|
|
186
193
|
def __draw__(self) -> None:
|
|
187
194
|
self.__dt__ = self.clock.tick(self.target_fps) / 1000.0
|
|
@@ -193,6 +200,11 @@ class RootEnv:
|
|
|
193
200
|
self.env.draw()
|
|
194
201
|
for util in self.utils.values(): util.__draw__()
|
|
195
202
|
|
|
203
|
+
# Save frame first (clean), then draw info overlay (not saved)
|
|
204
|
+
if hasattr(self, "__winrecorder__"):
|
|
205
|
+
self.__winrecorder__.update() # Captures frame without stats
|
|
206
|
+
self.__winrecorder__.draw() # Draws stats on screen only
|
|
207
|
+
|
|
196
208
|
pg.display.flip()
|
|
197
209
|
|
|
198
210
|
def __update__(self) -> None:
|
|
@@ -201,10 +213,6 @@ class RootEnv:
|
|
|
201
213
|
self.env.update()
|
|
202
214
|
for util in self.utils.values(): util.__update__()
|
|
203
215
|
|
|
204
|
-
if hasattr(self, "__winrecorder__"):
|
|
205
|
-
self.__winrecorder__.update()
|
|
206
|
-
self.__winrecorder__.draw()
|
|
207
|
-
|
|
208
216
|
def frame(self) -> None:
|
|
209
217
|
try:
|
|
210
218
|
self.events = pg.event.get()
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
from typing import Any, Callable, Literal
|
|
3
3
|
import pygame as pg
|
|
4
|
-
from e2D import *
|
|
5
|
-
from e2D.colors import *
|
|
4
|
+
from e2D import * # type: ignore
|
|
5
|
+
from e2D.colors import * # type: ignore
|
|
6
6
|
|
|
7
7
|
import math as _mt
|
|
8
8
|
|
|
@@ -18,6 +18,10 @@ def NEW_FONT(size, name:__LITERAL_FONTS__="arial", bold:bool=False, italic:bool=
|
|
|
18
18
|
FONT_ARIAL_16 = NEW_FONT(16)
|
|
19
19
|
FONT_ARIAL_32 = NEW_FONT(32)
|
|
20
20
|
FONT_ARIAL_64 = NEW_FONT(64)
|
|
21
|
+
FONT_MONOSPACE_16 = NEW_FONT(16, "cascadiamonoregular")
|
|
22
|
+
FONT_MONOSPACE_32 = NEW_FONT(32, "cascadiamonoregular")
|
|
23
|
+
FONT_MONOSPACE_64 = NEW_FONT(64, "cascadiamonoregular")
|
|
24
|
+
|
|
21
25
|
|
|
22
26
|
|
|
23
27
|
__LITERAL_PIVOT_POSITIONS__ = Literal["top_left", "top_center", "top_right", "center_left", "center_center", "center_right", "bottom_left", "bottom_center", "bottom_right"]
|
e2d-1.4.24/e2D/winrec.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
from typing import Optional, TYPE_CHECKING
|
|
2
|
+
from e2D import V2
|
|
3
|
+
from e2D.envs import RootEnv, FONT_MONOSPACE_16
|
|
4
|
+
from e2D.colors import BLACK_COLOR_PYG
|
|
5
|
+
import pygame as pg
|
|
6
|
+
import numpy as np
|
|
7
|
+
import cv2
|
|
8
|
+
import threading
|
|
9
|
+
import queue
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
class WinRec:
|
|
13
|
+
def __init__(self, rootEnv:RootEnv, fps:int=30, draw_on_screen:bool=True, path:str='output.mp4', font:pg.font.Font=FONT_MONOSPACE_16) -> None:
|
|
14
|
+
self.rootEnv = rootEnv
|
|
15
|
+
self.path = path
|
|
16
|
+
self.fps = fps
|
|
17
|
+
self.draw_on_screen = draw_on_screen
|
|
18
|
+
self.font = font
|
|
19
|
+
self.is_recording = True # Recording state (pause/resume)
|
|
20
|
+
self.screenshot_counter = 0 # Counter for screenshot filenames
|
|
21
|
+
self.recording_frames = 0 # Frames actually recorded (excludes paused frames)
|
|
22
|
+
self.pause_start_time = None # Track when recording was paused
|
|
23
|
+
self.total_pause_duration = 0.0 # Cumulative pause time
|
|
24
|
+
size = self.rootEnv.screen_size
|
|
25
|
+
self.video_writer = cv2.VideoWriter(self.path, cv2.VideoWriter_fourcc(*'mp4v'), self.fps, size()) #type: ignore
|
|
26
|
+
|
|
27
|
+
# Pre-allocate buffers for zero-copy operations
|
|
28
|
+
self.frame_buffer = np.empty(shape=(int(size.y), int(size.x), 3), dtype=np.uint8)
|
|
29
|
+
|
|
30
|
+
# Setup async video writing
|
|
31
|
+
self.frame_queue = queue.Queue(maxsize=120) # Buffer up to 4 seconds at 30fps
|
|
32
|
+
self.running = True
|
|
33
|
+
|
|
34
|
+
# Statistics tracking
|
|
35
|
+
self.frames_written = 0
|
|
36
|
+
self.frames_dropped = 0
|
|
37
|
+
self.write_start_time = time.time()
|
|
38
|
+
self.last_stat_update = time.time()
|
|
39
|
+
self.current_write_fps = 0.0
|
|
40
|
+
|
|
41
|
+
self.write_thread = threading.Thread(target=self._write_worker, daemon=False)
|
|
42
|
+
self.write_thread.start()
|
|
43
|
+
|
|
44
|
+
def _write_worker(self) -> None:
|
|
45
|
+
"""Background thread that writes frames to video file."""
|
|
46
|
+
while self.running or not self.frame_queue.empty():
|
|
47
|
+
try:
|
|
48
|
+
frame = self.frame_queue.get(timeout=0.1)
|
|
49
|
+
self.video_writer.write(frame)
|
|
50
|
+
self.frames_written += 1
|
|
51
|
+
self.frame_queue.task_done()
|
|
52
|
+
|
|
53
|
+
# Update write FPS every second
|
|
54
|
+
current_time = time.time()
|
|
55
|
+
if current_time - self.last_stat_update >= 1.0:
|
|
56
|
+
elapsed = current_time - self.write_start_time
|
|
57
|
+
self.current_write_fps = self.frames_written / elapsed if elapsed > 0 else 0
|
|
58
|
+
self.last_stat_update = current_time
|
|
59
|
+
except queue.Empty:
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
def handle_input(self) -> None:
|
|
63
|
+
"""Handle recording control keyboard inputs (F9-F12)."""
|
|
64
|
+
# F9: Toggle pause/resume recording
|
|
65
|
+
if self.rootEnv.keyboard.get_key(pg.K_F9, "just_pressed"):
|
|
66
|
+
self.toggle_recording()
|
|
67
|
+
status = "REC" if self.is_recording else "PAUSED"
|
|
68
|
+
print(f"[Recording] {status}")
|
|
69
|
+
|
|
70
|
+
# F10: Restart recording (reset all and resume)
|
|
71
|
+
if self.rootEnv.keyboard.get_key(pg.K_F10, "just_pressed"):
|
|
72
|
+
self.restart()
|
|
73
|
+
print("[Recording] Restarted (buffer cleared, timers reset)")
|
|
74
|
+
|
|
75
|
+
# F12: Take screenshot
|
|
76
|
+
if self.rootEnv.keyboard.get_key(pg.K_F12, "just_pressed"):
|
|
77
|
+
screenshot_path = self.take_screenshot()
|
|
78
|
+
print(f"[Screenshot] Saved: {screenshot_path}")
|
|
79
|
+
|
|
80
|
+
def update(self) -> None:
|
|
81
|
+
# Handle keyboard input first
|
|
82
|
+
self.handle_input()
|
|
83
|
+
|
|
84
|
+
# Skip frame capture if recording is paused
|
|
85
|
+
if not self.is_recording:
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
# Increment recording frame counter
|
|
89
|
+
self.recording_frames += 1
|
|
90
|
+
|
|
91
|
+
# Use pixels3d for zero-copy view, then transpose (creates view, not copy)
|
|
92
|
+
pixels = pg.surfarray.pixels3d(self.rootEnv.screen)
|
|
93
|
+
transposed = np.transpose(pixels, (1, 0, 2))
|
|
94
|
+
|
|
95
|
+
# Convert color in-place to pre-allocated buffer
|
|
96
|
+
cv2.cvtColor(transposed, cv2.COLOR_RGB2BGR, dst=self.frame_buffer)
|
|
97
|
+
|
|
98
|
+
# Queue frame copy for async writing (non-blocking)
|
|
99
|
+
try:
|
|
100
|
+
self.frame_queue.put_nowait(self.frame_buffer.copy())
|
|
101
|
+
except queue.Full:
|
|
102
|
+
self.frames_dropped += 1 # Track dropped frames
|
|
103
|
+
|
|
104
|
+
def get_rec_seconds(self) -> float:
|
|
105
|
+
"""Get recorded time in seconds (excludes paused time)."""
|
|
106
|
+
return self.recording_frames / self.fps
|
|
107
|
+
|
|
108
|
+
def draw(self) -> None:
|
|
109
|
+
# Calculate statistics
|
|
110
|
+
buffer_size = self.frame_queue.qsize()
|
|
111
|
+
buffer_percent = (buffer_size / self.frame_queue.maxsize) * 100
|
|
112
|
+
buffer_seconds = buffer_size / self.fps if self.fps > 0 else 0
|
|
113
|
+
|
|
114
|
+
# Estimate optimal buffer size based on write performance
|
|
115
|
+
if self.current_write_fps > 0 and self.fps > 0:
|
|
116
|
+
write_lag = self.fps / self.current_write_fps
|
|
117
|
+
estimated_buffer = int(self.fps * 2 * write_lag) # 2 seconds of lag compensation
|
|
118
|
+
else:
|
|
119
|
+
estimated_buffer = self.frame_queue.maxsize
|
|
120
|
+
|
|
121
|
+
# Recording state indicator
|
|
122
|
+
rec_status = "REC" if self.is_recording else "PAUSED"
|
|
123
|
+
|
|
124
|
+
# Format with fixed width for stable display (monospace-friendly)
|
|
125
|
+
row1 = (f"[{rec_status}] RecFrames:{self.recording_frames:>6} | "
|
|
126
|
+
f"RecTime:{self.get_rec_seconds():>6.2f}s | "
|
|
127
|
+
f"AppTime:{self.rootEnv.runtime_seconds:>6.2f}s")
|
|
128
|
+
row2 = (f"Buffer:{buffer_size:>3}/{self.frame_queue.maxsize:<3} ({buffer_percent:>5.1f}%, {buffer_seconds:>4.1f}s) | "
|
|
129
|
+
f"WriteFPS:{self.current_write_fps:>5.1f}")
|
|
130
|
+
row3 = (f"Written:{self.frames_written:>6} | Dropped:{self.frames_dropped:>4} | OptBuf:{estimated_buffer:>3}")
|
|
131
|
+
row4 = "[F9]Pause/Resume [F10]Restart [F12]Screenshot"
|
|
132
|
+
|
|
133
|
+
if self.draw_on_screen:
|
|
134
|
+
size = self.rootEnv.print(row4, self.rootEnv.screen_size - V2(16, 16), pivot_position='bottom_right', font=self.font, margin=V2(10, 10), bg_color=BLACK_COLOR_PYG, border_radius=10)
|
|
135
|
+
self.rootEnv.print(row3, self.rootEnv.screen_size - V2(16, 16 + size.y), pivot_position='bottom_right', font=self.font, margin=V2(10, 10), bg_color=BLACK_COLOR_PYG, border_radius=10)
|
|
136
|
+
self.rootEnv.print(row2, self.rootEnv.screen_size - V2(16, 16 + size.y * 2), pivot_position='bottom_right', font=self.font, margin=V2(10, 10), bg_color=BLACK_COLOR_PYG, border_radius=10)
|
|
137
|
+
self.rootEnv.print(row1, self.rootEnv.screen_size - V2(16, 16 + size.y * 3), pivot_position='bottom_right', font=self.font, margin=V2(10, 10), bg_color=BLACK_COLOR_PYG, border_radius=10)
|
|
138
|
+
|
|
139
|
+
def pause(self) -> None:
|
|
140
|
+
"""Pause recording (stop capturing frames)."""
|
|
141
|
+
if self.is_recording:
|
|
142
|
+
self.is_recording = False
|
|
143
|
+
self.pause_start_time = time.time()
|
|
144
|
+
|
|
145
|
+
def resume(self) -> None:
|
|
146
|
+
"""Resume recording (continue capturing frames)."""
|
|
147
|
+
if not self.is_recording and self.pause_start_time is not None:
|
|
148
|
+
self.total_pause_duration += time.time() - self.pause_start_time
|
|
149
|
+
self.pause_start_time = None
|
|
150
|
+
self.is_recording = True
|
|
151
|
+
|
|
152
|
+
def toggle_recording(self) -> None:
|
|
153
|
+
"""Toggle between pause and resume."""
|
|
154
|
+
self.is_recording = not self.is_recording
|
|
155
|
+
|
|
156
|
+
def restart(self) -> None:
|
|
157
|
+
"""Restart recording: clear buffer, reset all counters and timers, resume recording."""
|
|
158
|
+
self.clear_buffer()
|
|
159
|
+
self.recording_frames = 0
|
|
160
|
+
self.total_pause_duration = 0.0
|
|
161
|
+
self.pause_start_time = None
|
|
162
|
+
self.is_recording = True
|
|
163
|
+
|
|
164
|
+
def clear_buffer(self) -> None:
|
|
165
|
+
"""Clear the frame queue and reset write statistics."""
|
|
166
|
+
# Clear the queue
|
|
167
|
+
while not self.frame_queue.empty():
|
|
168
|
+
try:
|
|
169
|
+
self.frame_queue.get_nowait()
|
|
170
|
+
self.frame_queue.task_done()
|
|
171
|
+
except queue.Empty:
|
|
172
|
+
break
|
|
173
|
+
|
|
174
|
+
# Reset write counters (but keep recording frames)
|
|
175
|
+
self.frames_written = 0
|
|
176
|
+
self.frames_dropped = 0
|
|
177
|
+
self.write_start_time = time.time()
|
|
178
|
+
self.last_stat_update = time.time()
|
|
179
|
+
self.current_write_fps = 0.0
|
|
180
|
+
|
|
181
|
+
def take_screenshot(self, filename:Optional[str]=None) -> str:
|
|
182
|
+
"""Save current screen as PNG screenshot.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
filename: Optional custom filename. If None, auto-generates with counter.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
str: Path to saved screenshot
|
|
189
|
+
"""
|
|
190
|
+
if filename is None:
|
|
191
|
+
# Auto-generate filename with counter
|
|
192
|
+
base_path = self.path.rsplit('.', 1)[0] # Remove .mp4 extension
|
|
193
|
+
filename = f"{base_path}_screenshot_{self.screenshot_counter:04d}.png"
|
|
194
|
+
self.screenshot_counter += 1
|
|
195
|
+
|
|
196
|
+
# Capture current screen
|
|
197
|
+
pixels = pg.surfarray.pixels3d(self.rootEnv.screen)
|
|
198
|
+
transposed = np.transpose(pixels, (1, 0, 2))
|
|
199
|
+
screenshot_buffer = np.empty(shape=(int(self.rootEnv.screen_size.y), int(self.rootEnv.screen_size.x), 3), dtype=np.uint8)
|
|
200
|
+
cv2.cvtColor(transposed, cv2.COLOR_RGB2BGR, dst=screenshot_buffer)
|
|
201
|
+
|
|
202
|
+
# Save as PNG
|
|
203
|
+
cv2.imwrite(filename, screenshot_buffer)
|
|
204
|
+
return filename
|
|
205
|
+
|
|
206
|
+
def quit(self) -> None:
|
|
207
|
+
# Stop accepting new frames and wait for queue to flush
|
|
208
|
+
self.running = False
|
|
209
|
+
self.frame_queue.join() # Wait for all queued frames to be written
|
|
210
|
+
self.write_thread.join(timeout=5.0) # Wait for thread to finish
|
|
211
|
+
self.video_writer.release()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: e2D
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.24
|
|
4
4
|
Summary: Python library for 2D games. Streamlines dev with keyboard/mouse input, vector calculations, color manipulation, and collision detection. Simplify game creation and unleash creativity!
|
|
5
5
|
Home-page: https://github.com/marick-py/e2D
|
|
6
6
|
Author: Riccardo Mariani
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[metadata]
|
|
2
2
|
name = e2D
|
|
3
|
-
version = 1.4.
|
|
3
|
+
version = 1.4.24
|
|
4
4
|
author = Riccardo Mariani
|
|
5
5
|
author_email = ricomari2006@gmail.com
|
|
6
6
|
description = Python library for 2D games. Streamlines dev with keyboard/mouse input, vector calculations, color manipulation, and collision detection. Simplify game creation and unleash creativity!
|
e2d-1.4.22/e2D/winrec.py
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
from e2D.envs import *
|
|
2
|
-
import pygame as pg
|
|
3
|
-
import numpy as np
|
|
4
|
-
import cv2
|
|
5
|
-
|
|
6
|
-
class WinRec:
|
|
7
|
-
def __init__(self, rootEnv:RootEnv, fps:int=30, path:str='output.mp4') -> None:
|
|
8
|
-
self.rootEnv = rootEnv
|
|
9
|
-
self.path = path
|
|
10
|
-
self.fps = fps
|
|
11
|
-
self.video_writer = cv2.VideoWriter(self.path, cv2.VideoWriter_fourcc(*'mp4v'), self.fps, self.rootEnv.screen_size()) #type: ignore
|
|
12
|
-
|
|
13
|
-
def update(self) -> None:
|
|
14
|
-
frame = cv2.cvtColor(np.swapaxes(pg.surfarray.array3d(self.rootEnv.screen), 0, 1), cv2.COLOR_RGB2BGR)
|
|
15
|
-
self.video_writer.write(frame)
|
|
16
|
-
|
|
17
|
-
def get_rec_seconds(self) -> float:
|
|
18
|
-
return self.rootEnv.current_frame/self.fps
|
|
19
|
-
|
|
20
|
-
def draw(self, draw_on_screen=False) -> None:
|
|
21
|
-
text = f"[cfps:{self.rootEnv.current_frame} || realtime:{round(self.get_rec_seconds(),2)} || apptime:{round(self.rootEnv.runtime_seconds,2)}]"
|
|
22
|
-
pg.display.set_caption(text)
|
|
23
|
-
if draw_on_screen: self.rootEnv.print(text, self.rootEnv.screen_size, pivot_position='bottom_right')
|
|
24
|
-
|
|
25
|
-
def quit(self) -> None:
|
|
26
|
-
self.video_writer.release()
|
|
27
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|