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/winrec.py CHANGED
@@ -1,27 +1,212 @@
1
- from e2D.envs import *
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.video_writer = cv2.VideoWriter(self.path, cv2.VideoWriter_fourcc(*'mp4v'), self.fps, self.rootEnv.screen_size()) #type: ignore
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
- frame = cv2.cvtColor(np.swapaxes(pg.surfarray.array3d(self.rootEnv.screen), 0, 1), cv2.COLOR_RGB2BGR)
15
- self.video_writer.write(frame)
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
- return self.rootEnv.current_frame/self.fps
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
- 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')
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
- self.video_writer.release()
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.2
1
+ Metadata-Version: 2.4
2
2
  Name: e2D
3
- Version: 1.4.19
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,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.2)
2
+ Generator: setuptools (80.10.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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.
@@ -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,,