e2D 1.4.19__py3-none-any.whl → 1.4.23__py3-none-any.whl
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/__init__.py +79 -29
- e2D/__init__.pyi +293 -5
- e2D/colors.py +53 -23
- e2D/envs.py +91 -35
- e2D/utils.py +443 -53
- e2D/winrec.py +197 -12
- {e2d-1.4.19.dist-info → e2d-1.4.23.dist-info}/METADATA +3 -2
- e2d-1.4.23.dist-info/RECORD +13 -0
- {e2d-1.4.19.dist-info → e2d-1.4.23.dist-info}/WHEEL +1 -1
- {e2d-1.4.19.dist-info → e2d-1.4.23.dist-info/licenses}/LICENSE +21 -21
- e2d-1.4.19.dist-info/RECORD +0 -13
- {e2d-1.4.19.dist-info → e2d-1.4.23.dist-info}/top_level.txt +0 -0
e2D/winrec.py
CHANGED
|
@@ -1,27 +1,212 @@
|
|
|
1
|
-
from
|
|
1
|
+
from typing import Optional, TYPE_CHECKING
|
|
2
|
+
from e2D import V2
|
|
3
|
+
if TYPE_CHECKING:
|
|
4
|
+
from e2D.envs import RootEnv, FONT_MONOSPACE_16
|
|
5
|
+
from e2D.colors import BLACK_COLOR_PYG
|
|
2
6
|
import pygame as pg
|
|
3
7
|
import numpy as np
|
|
4
8
|
import cv2
|
|
9
|
+
import threading
|
|
10
|
+
import queue
|
|
11
|
+
import time
|
|
5
12
|
|
|
6
13
|
class WinRec:
|
|
7
|
-
def __init__(self, rootEnv:RootEnv, fps:int=30, path:str='output.mp4') -> None:
|
|
14
|
+
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:
|
|
8
15
|
self.rootEnv = rootEnv
|
|
9
16
|
self.path = path
|
|
10
17
|
self.fps = fps
|
|
11
|
-
self.
|
|
18
|
+
self.draw_on_screen = draw_on_screen
|
|
19
|
+
self.font = font
|
|
20
|
+
self.is_recording = True # Recording state (pause/resume)
|
|
21
|
+
self.screenshot_counter = 0 # Counter for screenshot filenames
|
|
22
|
+
self.recording_frames = 0 # Frames actually recorded (excludes paused frames)
|
|
23
|
+
self.pause_start_time = None # Track when recording was paused
|
|
24
|
+
self.total_pause_duration = 0.0 # Cumulative pause time
|
|
25
|
+
size = self.rootEnv.screen_size
|
|
26
|
+
self.video_writer = cv2.VideoWriter(self.path, cv2.VideoWriter_fourcc(*'mp4v'), self.fps, size()) #type: ignore
|
|
27
|
+
|
|
28
|
+
# Pre-allocate buffers for zero-copy operations
|
|
29
|
+
self.frame_buffer = np.empty(shape=(int(size.y), int(size.x), 3), dtype=np.uint8)
|
|
30
|
+
|
|
31
|
+
# Setup async video writing
|
|
32
|
+
self.frame_queue = queue.Queue(maxsize=120) # Buffer up to 4 seconds at 30fps
|
|
33
|
+
self.running = True
|
|
34
|
+
|
|
35
|
+
# Statistics tracking
|
|
36
|
+
self.frames_written = 0
|
|
37
|
+
self.frames_dropped = 0
|
|
38
|
+
self.write_start_time = time.time()
|
|
39
|
+
self.last_stat_update = time.time()
|
|
40
|
+
self.current_write_fps = 0.0
|
|
41
|
+
|
|
42
|
+
self.write_thread = threading.Thread(target=self._write_worker, daemon=False)
|
|
43
|
+
self.write_thread.start()
|
|
44
|
+
|
|
45
|
+
def _write_worker(self) -> None:
|
|
46
|
+
"""Background thread that writes frames to video file."""
|
|
47
|
+
while self.running or not self.frame_queue.empty():
|
|
48
|
+
try:
|
|
49
|
+
frame = self.frame_queue.get(timeout=0.1)
|
|
50
|
+
self.video_writer.write(frame)
|
|
51
|
+
self.frames_written += 1
|
|
52
|
+
self.frame_queue.task_done()
|
|
53
|
+
|
|
54
|
+
# Update write FPS every second
|
|
55
|
+
current_time = time.time()
|
|
56
|
+
if current_time - self.last_stat_update >= 1.0:
|
|
57
|
+
elapsed = current_time - self.write_start_time
|
|
58
|
+
self.current_write_fps = self.frames_written / elapsed if elapsed > 0 else 0
|
|
59
|
+
self.last_stat_update = current_time
|
|
60
|
+
except queue.Empty:
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
def handle_input(self) -> None:
|
|
64
|
+
"""Handle recording control keyboard inputs (F9-F12)."""
|
|
65
|
+
# F9: Toggle pause/resume recording
|
|
66
|
+
if self.rootEnv.keyboard.get_key(pg.K_F9, "just_pressed"):
|
|
67
|
+
self.toggle_recording()
|
|
68
|
+
status = "REC" if self.is_recording else "PAUSED"
|
|
69
|
+
print(f"[Recording] {status}")
|
|
70
|
+
|
|
71
|
+
# F10: Restart recording (reset all and resume)
|
|
72
|
+
if self.rootEnv.keyboard.get_key(pg.K_F10, "just_pressed"):
|
|
73
|
+
self.restart()
|
|
74
|
+
print("[Recording] Restarted (buffer cleared, timers reset)")
|
|
75
|
+
|
|
76
|
+
# F12: Take screenshot
|
|
77
|
+
if self.rootEnv.keyboard.get_key(pg.K_F12, "just_pressed"):
|
|
78
|
+
screenshot_path = self.take_screenshot()
|
|
79
|
+
print(f"[Screenshot] Saved: {screenshot_path}")
|
|
12
80
|
|
|
13
81
|
def update(self) -> None:
|
|
14
|
-
|
|
15
|
-
self.
|
|
82
|
+
# Handle keyboard input first
|
|
83
|
+
self.handle_input()
|
|
84
|
+
|
|
85
|
+
# Skip frame capture if recording is paused
|
|
86
|
+
if not self.is_recording:
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
# Increment recording frame counter
|
|
90
|
+
self.recording_frames += 1
|
|
91
|
+
|
|
92
|
+
# Use pixels3d for zero-copy view, then transpose (creates view, not copy)
|
|
93
|
+
pixels = pg.surfarray.pixels3d(self.rootEnv.screen)
|
|
94
|
+
transposed = np.transpose(pixels, (1, 0, 2))
|
|
95
|
+
|
|
96
|
+
# Convert color in-place to pre-allocated buffer
|
|
97
|
+
cv2.cvtColor(transposed, cv2.COLOR_RGB2BGR, dst=self.frame_buffer)
|
|
98
|
+
|
|
99
|
+
# Queue frame copy for async writing (non-blocking)
|
|
100
|
+
try:
|
|
101
|
+
self.frame_queue.put_nowait(self.frame_buffer.copy())
|
|
102
|
+
except queue.Full:
|
|
103
|
+
self.frames_dropped += 1 # Track dropped frames
|
|
16
104
|
|
|
17
105
|
def get_rec_seconds(self) -> float:
|
|
18
|
-
|
|
106
|
+
"""Get recorded time in seconds (excludes paused time)."""
|
|
107
|
+
return self.recording_frames / self.fps
|
|
108
|
+
|
|
109
|
+
def draw(self) -> None:
|
|
110
|
+
# Calculate statistics
|
|
111
|
+
buffer_size = self.frame_queue.qsize()
|
|
112
|
+
buffer_percent = (buffer_size / self.frame_queue.maxsize) * 100
|
|
113
|
+
buffer_seconds = buffer_size / self.fps if self.fps > 0 else 0
|
|
114
|
+
|
|
115
|
+
# Estimate optimal buffer size based on write performance
|
|
116
|
+
if self.current_write_fps > 0 and self.fps > 0:
|
|
117
|
+
write_lag = self.fps / self.current_write_fps
|
|
118
|
+
estimated_buffer = int(self.fps * 2 * write_lag) # 2 seconds of lag compensation
|
|
119
|
+
else:
|
|
120
|
+
estimated_buffer = self.frame_queue.maxsize
|
|
121
|
+
|
|
122
|
+
# Recording state indicator
|
|
123
|
+
rec_status = "REC" if self.is_recording else "PAUSED"
|
|
124
|
+
|
|
125
|
+
# Format with fixed width for stable display (monospace-friendly)
|
|
126
|
+
row1 = (f"[{rec_status}] RecFrames:{self.recording_frames:>6} | "
|
|
127
|
+
f"RecTime:{self.get_rec_seconds():>6.2f}s | "
|
|
128
|
+
f"AppTime:{self.rootEnv.runtime_seconds:>6.2f}s")
|
|
129
|
+
row2 = (f"Buffer:{buffer_size:>3}/{self.frame_queue.maxsize:<3} ({buffer_percent:>5.1f}%, {buffer_seconds:>4.1f}s) | "
|
|
130
|
+
f"WriteFPS:{self.current_write_fps:>5.1f}")
|
|
131
|
+
row3 = (f"Written:{self.frames_written:>6} | Dropped:{self.frames_dropped:>4} | OptBuf:{estimated_buffer:>3}")
|
|
132
|
+
row4 = "[F9]Pause/Resume [F10]Restart [F12]Screenshot"
|
|
19
133
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
134
|
+
if self.draw_on_screen:
|
|
135
|
+
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)
|
|
136
|
+
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)
|
|
137
|
+
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)
|
|
138
|
+
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)
|
|
139
|
+
|
|
140
|
+
def pause(self) -> None:
|
|
141
|
+
"""Pause recording (stop capturing frames)."""
|
|
142
|
+
if self.is_recording:
|
|
143
|
+
self.is_recording = False
|
|
144
|
+
self.pause_start_time = time.time()
|
|
145
|
+
|
|
146
|
+
def resume(self) -> None:
|
|
147
|
+
"""Resume recording (continue capturing frames)."""
|
|
148
|
+
if not self.is_recording and self.pause_start_time is not None:
|
|
149
|
+
self.total_pause_duration += time.time() - self.pause_start_time
|
|
150
|
+
self.pause_start_time = None
|
|
151
|
+
self.is_recording = True
|
|
152
|
+
|
|
153
|
+
def toggle_recording(self) -> None:
|
|
154
|
+
"""Toggle between pause and resume."""
|
|
155
|
+
self.is_recording = not self.is_recording
|
|
156
|
+
|
|
157
|
+
def restart(self) -> None:
|
|
158
|
+
"""Restart recording: clear buffer, reset all counters and timers, resume recording."""
|
|
159
|
+
self.clear_buffer()
|
|
160
|
+
self.recording_frames = 0
|
|
161
|
+
self.total_pause_duration = 0.0
|
|
162
|
+
self.pause_start_time = None
|
|
163
|
+
self.is_recording = True
|
|
164
|
+
|
|
165
|
+
def clear_buffer(self) -> None:
|
|
166
|
+
"""Clear the frame queue and reset write statistics."""
|
|
167
|
+
# Clear the queue
|
|
168
|
+
while not self.frame_queue.empty():
|
|
169
|
+
try:
|
|
170
|
+
self.frame_queue.get_nowait()
|
|
171
|
+
self.frame_queue.task_done()
|
|
172
|
+
except queue.Empty:
|
|
173
|
+
break
|
|
174
|
+
|
|
175
|
+
# Reset write counters (but keep recording frames)
|
|
176
|
+
self.frames_written = 0
|
|
177
|
+
self.frames_dropped = 0
|
|
178
|
+
self.write_start_time = time.time()
|
|
179
|
+
self.last_stat_update = time.time()
|
|
180
|
+
self.current_write_fps = 0.0
|
|
181
|
+
|
|
182
|
+
def take_screenshot(self, filename:Optional[str]=None) -> str:
|
|
183
|
+
"""Save current screen as PNG screenshot.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
filename: Optional custom filename. If None, auto-generates with counter.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
str: Path to saved screenshot
|
|
190
|
+
"""
|
|
191
|
+
if filename is None:
|
|
192
|
+
# Auto-generate filename with counter
|
|
193
|
+
base_path = self.path.rsplit('.', 1)[0] # Remove .mp4 extension
|
|
194
|
+
filename = f"{base_path}_screenshot_{self.screenshot_counter:04d}.png"
|
|
195
|
+
self.screenshot_counter += 1
|
|
196
|
+
|
|
197
|
+
# Capture current screen
|
|
198
|
+
pixels = pg.surfarray.pixels3d(self.rootEnv.screen)
|
|
199
|
+
transposed = np.transpose(pixels, (1, 0, 2))
|
|
200
|
+
screenshot_buffer = np.empty(shape=(int(self.rootEnv.screen_size.y), int(self.rootEnv.screen_size.x), 3), dtype=np.uint8)
|
|
201
|
+
cv2.cvtColor(transposed, cv2.COLOR_RGB2BGR, dst=screenshot_buffer)
|
|
202
|
+
|
|
203
|
+
# Save as PNG
|
|
204
|
+
cv2.imwrite(filename, screenshot_buffer)
|
|
205
|
+
return filename
|
|
24
206
|
|
|
25
207
|
def quit(self) -> None:
|
|
26
|
-
|
|
27
|
-
|
|
208
|
+
# Stop accepting new frames and wait for queue to flush
|
|
209
|
+
self.running = False
|
|
210
|
+
self.frame_queue.join() # Wait for all queued frames to be written
|
|
211
|
+
self.write_thread.join(timeout=5.0) # Wait for thread to finish
|
|
212
|
+
self.video_writer.release()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: e2D
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.23
|
|
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
|
|
@@ -13,6 +13,7 @@ Description-Content-Type: text/markdown
|
|
|
13
13
|
License-File: LICENSE
|
|
14
14
|
Requires-Dist: numpy
|
|
15
15
|
Requires-Dist: pygame
|
|
16
|
+
Dynamic: license-file
|
|
16
17
|
|
|
17
18
|
# e2D
|
|
18
19
|
## A Python Game Development Library
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
e2D/__init__.py,sha256=ZTja5nMPyg3_fulzuSWhjA-JexSG9QHHr3sc0w7Qcpk,23857
|
|
2
|
+
e2D/__init__.pyi,sha256=1WNOYOyNyroL2LLrzE7CIJ7uubh_zhVbMvJINpy5PIY,53979
|
|
3
|
+
e2D/colors.py,sha256=4ZFcp6ZfdQ2eG3INw_Hx-KI2PkVustZ5TurnooHGSvw,19063
|
|
4
|
+
e2D/def_colors.py,sha256=3sJq2L6qFZ3svn2qEWIx0SinNXjb9huNaFigDeJipm8,43805
|
|
5
|
+
e2D/envs.py,sha256=5ah8SW9HNC3NqqZZfCBZs1BWtWHLA43dWwQruawzm9U,9240
|
|
6
|
+
e2D/plots.py,sha256=_d72ZJo-GIhcJ44XCphFH288cf_ZSwWcbLh_olgGjBc,35880
|
|
7
|
+
e2D/utils.py,sha256=Yamt6_JS6MVKrNtfzjP7BRWjCeXHuzFcFlL7H2Au7_M,29687
|
|
8
|
+
e2D/winrec.py,sha256=JzHW-fbnuyUoPG5_hoGe7RNBG4WnxjvjHNT2vJxiAGk,9767
|
|
9
|
+
e2d-1.4.23.dist-info/licenses/LICENSE,sha256=hbjljn38VVW9en51B0qzRK-v2FBDijqRWbZIVTk7ipU,1094
|
|
10
|
+
e2d-1.4.23.dist-info/METADATA,sha256=_nwwI7muu3u9xkJvrpHlRUOA0Ofskf5IfXohLZY5KBA,9634
|
|
11
|
+
e2d-1.4.23.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
|
|
12
|
+
e2d-1.4.23.dist-info/top_level.txt,sha256=3vKZ-CGzNlTCpzVMmM0Ht76krCofKw7hZ0wBf-dnKdM,4
|
|
13
|
+
e2d-1.4.23.dist-info/RECORD,,
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 Riccardo Mariani
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Riccardo Mariani
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
e2d-1.4.19.dist-info/RECORD
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
e2D/__init__.py,sha256=Ex3kVbhzHPqoeGhVJr83TAlwRfA65iaoaA_wuKaq1bs,22080
|
|
2
|
-
e2D/__init__.pyi,sha256=SgTFu2fx0qOAp8BbiCT8kd6ySzKwjpZqhfcPlE_pTEE,43472
|
|
3
|
-
e2D/colors.py,sha256=DgkgUdaQY41nA0VlJaMaT6VZwypG--Cw3Pwakf4OVHM,17412
|
|
4
|
-
e2D/def_colors.py,sha256=3sJq2L6qFZ3svn2qEWIx0SinNXjb9huNaFigDeJipm8,43805
|
|
5
|
-
e2D/envs.py,sha256=VmXKHcMBPHArx9u15mZtbAb7eXWdmhSC2nz4y_sRvro,6349
|
|
6
|
-
e2D/plots.py,sha256=_d72ZJo-GIhcJ44XCphFH288cf_ZSwWcbLh_olgGjBc,35880
|
|
7
|
-
e2D/utils.py,sha256=cJarYc6OTIdud7AJZHxwOhxMcEJLlgfKu60kkBu4hB8,14116
|
|
8
|
-
e2D/winrec.py,sha256=EFFfWYbk27NhS-rWD-BLChXvLjFW1uYZ5LkRGMj_Xo0,1116
|
|
9
|
-
e2d-1.4.19.dist-info/LICENSE,sha256=VP36drkzlpF7Kc7qhWiQ-Foke1Ru3WO0e5JgASurHNM,1073
|
|
10
|
-
e2d-1.4.19.dist-info/METADATA,sha256=4Rg4PBPfe_WOlzDT0CPT5450sjo5MX8oIECWWcNy4TQ,9611
|
|
11
|
-
e2d-1.4.19.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
|
|
12
|
-
e2d-1.4.19.dist-info/top_level.txt,sha256=3vKZ-CGzNlTCpzVMmM0Ht76krCofKw7hZ0wBf-dnKdM,4
|
|
13
|
-
e2d-1.4.19.dist-info/RECORD,,
|
|
File without changes
|