RGBMatrixEmulator 0.12.0__py2.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.
Files changed (42) hide show
  1. RGBMatrixEmulator/__init__.py +5 -0
  2. RGBMatrixEmulator/adapters/__init__.py +72 -0
  3. RGBMatrixEmulator/adapters/base.py +109 -0
  4. RGBMatrixEmulator/adapters/browser_adapter/README.md +89 -0
  5. RGBMatrixEmulator/adapters/browser_adapter/__init__.py +0 -0
  6. RGBMatrixEmulator/adapters/browser_adapter/adapter.py +51 -0
  7. RGBMatrixEmulator/adapters/browser_adapter/request_handlers/__init__.py +7 -0
  8. RGBMatrixEmulator/adapters/browser_adapter/request_handlers/image.py +12 -0
  9. RGBMatrixEmulator/adapters/browser_adapter/request_handlers/image_web_socket.py +34 -0
  10. RGBMatrixEmulator/adapters/browser_adapter/request_handlers/main.py +9 -0
  11. RGBMatrixEmulator/adapters/browser_adapter/server.py +86 -0
  12. RGBMatrixEmulator/adapters/browser_adapter/static/assets/client.js +91 -0
  13. RGBMatrixEmulator/adapters/browser_adapter/static/assets/icon.ico +0 -0
  14. RGBMatrixEmulator/adapters/browser_adapter/static/assets/styles.css +25 -0
  15. RGBMatrixEmulator/adapters/browser_adapter/static/index.html +144 -0
  16. RGBMatrixEmulator/adapters/pygame_adapter.py +59 -0
  17. RGBMatrixEmulator/adapters/sixel_adapter.py +90 -0
  18. RGBMatrixEmulator/adapters/terminal_adapter.py +34 -0
  19. RGBMatrixEmulator/adapters/tkinter_adapter.py +83 -0
  20. RGBMatrixEmulator/adapters/turtle_adapter.py +97 -0
  21. RGBMatrixEmulator/emulation/__init__.py +0 -0
  22. RGBMatrixEmulator/emulation/canvas.py +71 -0
  23. RGBMatrixEmulator/emulation/matrix.py +62 -0
  24. RGBMatrixEmulator/emulation/options.py +213 -0
  25. RGBMatrixEmulator/graphics/__init__.py +169 -0
  26. RGBMatrixEmulator/graphics/color.py +32 -0
  27. RGBMatrixEmulator/graphics/font.py +36 -0
  28. RGBMatrixEmulator/icon.ico +0 -0
  29. RGBMatrixEmulator/icon.png +0 -0
  30. RGBMatrixEmulator/logger.py +28 -0
  31. RGBMatrixEmulator/version.py +5 -0
  32. rgbmatrixemulator-0.12.0.data/data/RGBMatrixEmulator/client.js +91 -0
  33. rgbmatrixemulator-0.12.0.data/data/RGBMatrixEmulator/icon.ico +0 -0
  34. rgbmatrixemulator-0.12.0.data/data/RGBMatrixEmulator/icon.png +0 -0
  35. rgbmatrixemulator-0.12.0.data/data/RGBMatrixEmulator/index.html +144 -0
  36. rgbmatrixemulator-0.12.0.data/data/RGBMatrixEmulator/styles.css +25 -0
  37. rgbmatrixemulator-0.12.0.data/data/docs/LICENSE +9 -0
  38. rgbmatrixemulator-0.12.0.data/data/docs/README.md +160 -0
  39. rgbmatrixemulator-0.12.0.dist-info/METADATA +186 -0
  40. rgbmatrixemulator-0.12.0.dist-info/RECORD +43 -0
  41. rgbmatrixemulator-0.12.0.dist-info/WHEEL +5 -0
  42. rgbmatrixemulator-0.12.0.dist-info/licenses/LICENSE +9 -0
@@ -0,0 +1,144 @@
1
+ <html>
2
+ <head>
3
+ <title>{{ adapter.emulator_details_text() }}</title>
4
+ <link rel="shortcut icon" href="assets/icon.ico">
5
+ <link rel="stylesheet" href="assets/styles.css">
6
+ </head>
7
+ <body>
8
+ <img id="liveImg" class={{ "" if adapter.options.browser.image_border else "no-border" }} />
9
+
10
+ {% if adapter.options.browser.fps_display %}
11
+ <div id="fpsDisplay">
12
+ Frames per second: <span id="fps">0</span>
13
+ </div>
14
+ {% end %}
15
+
16
+ {% if adapter.options.browser.debug_text %}
17
+ <div id="emulatorDetails">
18
+ <h2>{{ adapter.emulator_details_text() }}</h2>
19
+
20
+ <div class="emulatorDetailsTableRow">
21
+ <div id="matrixDetails" class="tableContainer">
22
+ <h3>Matrix Details:</h3>
23
+
24
+ <table>
25
+ <tbody>
26
+ <tr>
27
+ <td>
28
+ Matrix Width:
29
+ </td>
30
+ <td>
31
+ {{ adapter.options.cols }}
32
+ </td>
33
+ </tr>
34
+ <tr>
35
+ <td>
36
+ Matrix Height:
37
+ </td>
38
+ <td>
39
+ {{ adapter.options.rows }}
40
+ </td>
41
+ </td>
42
+ </tr>
43
+ <tr>
44
+ <td>
45
+ Image Size:
46
+ </td>
47
+ <td>
48
+ {{ adapter.options.window_size_str(pixel_text="px") }}
49
+ </td>
50
+ </tr>
51
+ <tr>
52
+ <td>
53
+ Chain Length:
54
+ </td>
55
+ <td>
56
+ {{ adapter.options.chain_length }}
57
+ </td>
58
+ </tr>
59
+ <tr>
60
+ <td>
61
+ Parallel Chains:
62
+ </td>
63
+ <td>
64
+ {{ adapter.options.parallel }}
65
+ </td>
66
+ </tr>
67
+ <tr>
68
+ <td>
69
+ Pixel Size:
70
+ </td>
71
+ <td>
72
+ {{ adapter.options.pixel_size }}
73
+ </td>
74
+ </tr>
75
+ <tr>
76
+ <td>
77
+ Pixel Style:
78
+ </td>
79
+ <td>
80
+ {{ adapter.options.pixel_style }}
81
+ </td>
82
+ </tr>
83
+ <tr>
84
+ <td>
85
+ Adapter:
86
+ </td>
87
+ <td>
88
+ {{ adapter.__class__.__name__ }}
89
+ </td>
90
+ </tr>
91
+ </tbody>
92
+ </table>
93
+ </div>
94
+
95
+ <div id="browserDetails" class="tableContainer">
96
+ <h3>Browser Details:</h3>
97
+
98
+ <table>
99
+ <tbody>
100
+ <tr>
101
+ <td>
102
+ Port:
103
+ </td>
104
+ <td>
105
+ {{ adapter.options.browser.port }}
106
+ </td>
107
+ </tr>
108
+ <tr>
109
+ <td>
110
+ Target FPS:
111
+ </td>
112
+ <td>
113
+ {{ adapter.options.browser.target_fps }}
114
+ </td>
115
+ </td>
116
+ </tr>
117
+ <tr>
118
+ <td>
119
+ FPS Display Enabled:
120
+ </td>
121
+ <td>
122
+ {{ adapter.options.browser.fps_display }}
123
+ </td>
124
+ </tr>
125
+ <tr>
126
+ <td>
127
+ Image Quality:
128
+ </td>
129
+ <td>
130
+ {{ adapter.options.browser.quality }}
131
+ </td>
132
+ </tr>
133
+ </tbody>
134
+ </table>
135
+ </div>
136
+ </div>
137
+ </div>
138
+ {% end %}
139
+
140
+ <input id="targetFps" type="hidden" value={{ adapter.options.browser.target_fps }} />
141
+
142
+ <script type="text/javascript" src="assets/client.js"></script>
143
+ </body>
144
+ </html>
@@ -0,0 +1,59 @@
1
+ import os
2
+ import sys
3
+
4
+ # Try to suppress the pygame load warning if able.
5
+ try:
6
+ os.environ["PYGAME_HIDE_SUPPORT_PROMPT"] = "hide"
7
+ except Exception:
8
+ pass
9
+
10
+ import pygame
11
+
12
+ from pygame.locals import QUIT
13
+ from RGBMatrixEmulator.adapters.base import BaseAdapter
14
+ from RGBMatrixEmulator.logger import Logger
15
+
16
+
17
+ class PygameAdapter(BaseAdapter):
18
+ SUPPORTS_ALTERNATE_PIXEL_STYLE = True
19
+
20
+ def __init__(self, width, height, options):
21
+ super().__init__(width, height, options)
22
+ self.__surface = None
23
+
24
+ def load_emulator_window(self):
25
+ if self.loaded:
26
+ return
27
+
28
+ Logger.info("Loading {}".format(self.emulator_details_text()))
29
+ self.__surface = pygame.display.set_mode(self.options.window_size())
30
+ pygame.init()
31
+
32
+ self.__set_emulator_icon()
33
+ pygame.display.set_caption(self.emulator_details_text())
34
+
35
+ self.loaded = True
36
+
37
+ def draw_to_screen(self, pixels):
38
+ image = self._get_masked_image(pixels)
39
+ pygame_surface = pygame.image.fromstring(
40
+ image.tobytes(), self.options.window_size(), "RGB"
41
+ )
42
+ self.__surface.blit(pygame_surface, (0, 0))
43
+
44
+ pygame.display.flip()
45
+
46
+ def check_for_quit_event(self):
47
+ # We don't have events, but this will keep the emulator from appearing as if it's not responding.
48
+ # This also enables closing the window to kill the emulator
49
+ for event in pygame.event.get():
50
+ if event.type == QUIT:
51
+ pygame.quit()
52
+ sys.exit()
53
+
54
+ def __set_emulator_icon(self):
55
+ emulator_path = os.path.abspath(os.path.dirname(__file__))
56
+ icon_path = os.path.join(emulator_path, "..", "icon.png")
57
+ icon = pygame.image.load(os.path.normpath(icon_path))
58
+
59
+ pygame.display.set_icon(icon)
@@ -0,0 +1,90 @@
1
+ import os
2
+ import platform
3
+ import sys
4
+ import io
5
+ import libsixel as sixel
6
+
7
+ from PIL import Image, ImageDraw, ImageEnhance
8
+ from typing import List
9
+
10
+ from RGBMatrixEmulator.adapters.base import BaseAdapter
11
+ from RGBMatrixEmulator.graphics.color import Color
12
+
13
+
14
+ class SixelAdapter(BaseAdapter):
15
+ SUPPORTS_ALTERNATE_PIXEL_STYLE = True
16
+
17
+ def draw_to_screen(self, pixels):
18
+ sixel = self.__encode_sixels(
19
+ pixels, scale=self.options.pixel_size, outline=self.options.pixel_outline
20
+ )
21
+ output = f"\033[H\n{sixel}\n"
22
+ sys.stdout.write(output)
23
+
24
+ def load_emulator_window(self):
25
+ os.system("cls||clear")
26
+ if platform.system() == "Windows":
27
+ os.system(
28
+ "mode con: cols={} lines={}".format(self.width * 2 + 5, self.height + 3)
29
+ )
30
+ else:
31
+ os.system(
32
+ "stty columns {} rows {}".format(self.width * 2 + 5, self.height + 3)
33
+ )
34
+
35
+ def __enlarge_pixels(
36
+ self, pixels: List[List[Color]], scale: int, outline: int
37
+ ) -> Image.Image:
38
+ outline_color = "#000000"
39
+ w, h = len(pixels[0]), len(pixels)
40
+ iw, ih = w * scale, h * scale
41
+ i = Image.new("RGBA", (iw, ih))
42
+ d = ImageDraw.Draw(i)
43
+ for y, row in enumerate(pixels):
44
+ for x, pixel in enumerate(row):
45
+ rx, ry = x * scale, y * scale
46
+ ex, ey = rx + (scale - 1), ry + (scale - 1)
47
+ if self.options.pixel_style == "circle":
48
+ d.ellipse(
49
+ [(rx, ry), (ex, ey)],
50
+ fill=pixel,
51
+ outline=outline_color,
52
+ width=outline,
53
+ )
54
+ else:
55
+ d.rectangle(
56
+ [(rx, ry), (ex, ey)],
57
+ fill=pixel,
58
+ outline=outline_color,
59
+ width=outline,
60
+ )
61
+
62
+ brightness = ImageEnhance.Brightness(i)
63
+ i = brightness.enhance(1.5)
64
+ contrast = ImageEnhance.Contrast(i)
65
+ i = contrast.enhance(0.8)
66
+
67
+ return i
68
+
69
+ def __encode_sixels(self, pixels: List[List[Color]], scale=4, outline=1) -> str:
70
+ """Encodes given Image to a sixel string."""
71
+ img = self.__enlarge_pixels(pixels, scale, outline)
72
+ img_data = img.convert("RGB").tobytes()
73
+ width = img.width
74
+ height = img.height
75
+ with io.BytesIO() as buf:
76
+ output = sixel.sixel_output_new(
77
+ lambda data, buffer: buffer.write(data), buf
78
+ )
79
+ dither = sixel.sixel_dither_new(256)
80
+ sixel.sixel_dither_initialize(
81
+ dither, img_data, width, height, sixel.SIXEL_PIXELFORMAT_RGB888
82
+ )
83
+ sixel.sixel_encode(img_data, width, height, 1, dither, output)
84
+
85
+ encoded = buf.getvalue().decode("ascii")
86
+
87
+ sixel.sixel_dither_unref(dither)
88
+ sixel.sixel_output_unref(output)
89
+
90
+ return encoded
@@ -0,0 +1,34 @@
1
+ import os
2
+ import sys
3
+
4
+ from RGBMatrixEmulator.adapters.base import BaseAdapter
5
+
6
+
7
+ class TerminalAdapter(BaseAdapter):
8
+ SUPPORTS_ALTERNATE_PIXEL_STYLE = True
9
+ SYMBOLS = {"circle": " ●", "square": "██"}
10
+
11
+ def __init__(self, width, height, options):
12
+ super().__init__(width, height, options)
13
+ self.__symbol = self.SYMBOLS.get(self.options.pixel_style)
14
+
15
+ def draw_to_screen(self, pixels):
16
+ output = "\033[H\n" # Move the cursor to the home position, add a little border
17
+ for pixel_row in pixels:
18
+ output += " " # Add a bit of border in case cursor causes line to wrap
19
+ for pixel in pixel_row:
20
+ output += "\033[38;2;{};{};{}m".format(
21
+ *pixel
22
+ ) # Set the cell to the pixel color
23
+ output += self.__symbol # Draw the pixel
24
+ output += "\033[37m" # Reset the color
25
+
26
+ output += " \n"
27
+
28
+ sys.stdout.write(output)
29
+
30
+ def load_emulator_window(self):
31
+ os.system("cls||clear")
32
+ os.system(
33
+ "mode con: cols={} lines={}".format(self.width * 2 + 5, self.height + 3)
34
+ )
@@ -0,0 +1,83 @@
1
+ import tkinter
2
+ import os
3
+
4
+ from RGBMatrixEmulator.adapters.base import BaseAdapter
5
+ from RGBMatrixEmulator.graphics import Color
6
+ from RGBMatrixEmulator.logger import Logger
7
+
8
+
9
+ class TkinterAdapter(BaseAdapter):
10
+ SUPPORTS_ALTERNATE_PIXEL_STYLE = True
11
+
12
+ def __init__(self, width, height, options):
13
+ super().__init__(width, height, options)
14
+ self.__root = None
15
+ self.__canvas = None
16
+ self.__pixels = None
17
+
18
+ def load_emulator_window(self):
19
+ if self.loaded:
20
+ return
21
+
22
+ Logger.info("Loading {}".format(self.emulator_details_text()))
23
+ self.__root = tkinter.Tk()
24
+ self.__set_emulator_icon()
25
+ self.__root.title(self.emulator_details_text())
26
+
27
+ window_size = self.options.window_size()
28
+ self.__root.geometry("{}x{}".format(*window_size))
29
+ self.__canvas = tkinter.Canvas(
30
+ self.__root,
31
+ width=window_size[0],
32
+ height=window_size[1],
33
+ bd=0,
34
+ highlightthickness=0,
35
+ bg="black",
36
+ )
37
+
38
+ self.__initialize_bitmap()
39
+ self.__root.update()
40
+
41
+ self.loaded = True
42
+
43
+ def draw_to_screen(self, pixels):
44
+ for row, pixel_row in enumerate(pixels):
45
+ for col, pixel in enumerate(pixel_row):
46
+ shape_id = self.__pixels[row][col]
47
+
48
+ self.__canvas.itemconfig(shape_id, fill=Color.to_hex(pixel))
49
+
50
+ self.__canvas.pack()
51
+ self.__root.update()
52
+
53
+ def __initialize_bitmap(self):
54
+ self.__pixels = []
55
+
56
+ for row in range(0, self.height):
57
+ new_row = []
58
+
59
+ for col in range(0, self.width):
60
+ coords = self.__pixel_dimensions(col, row)
61
+
62
+ if self.options.pixel_style == "circle":
63
+ id = self.__canvas.create_oval(coords, width=0)
64
+ else:
65
+ id = self.__canvas.create_rectangle(coords, width=0)
66
+
67
+ new_row.append(id)
68
+
69
+ self.__pixels.append(new_row)
70
+
71
+ def __pixel_dimensions(self, col, row):
72
+ size = self.options.pixel_size
73
+ start, stop = (col * size, row * size)
74
+
75
+ return (start, stop, start + size, stop + size)
76
+
77
+ def __set_emulator_icon(self):
78
+ emulator_path = os.path.abspath(os.path.dirname(__file__))
79
+ raw_icon_path = os.path.join(emulator_path, "..", "icon.png")
80
+ icon_path = os.path.normpath(raw_icon_path)
81
+
82
+ icon = tkinter.PhotoImage(file=icon_path)
83
+ self.__root.tk.call("wm", "iconphoto", self.__root._w, icon)
@@ -0,0 +1,97 @@
1
+ import os
2
+ import tkinter
3
+ import turtle
4
+
5
+ from RGBMatrixEmulator.adapters.base import BaseAdapter
6
+ from RGBMatrixEmulator.graphics.color import Color
7
+ from RGBMatrixEmulator.logger import Logger
8
+
9
+
10
+ class TurtleAdapter(BaseAdapter):
11
+ def __init__(self, width, height, options):
12
+ super().__init__(width, height, options)
13
+ self.__pen = None
14
+ self.__screen = None
15
+
16
+ def draw_to_screen(self, pixels):
17
+ self.__pen.clear()
18
+
19
+ for row, pixel_row in enumerate(pixels):
20
+ self.__move_pen_to_row_start(row)
21
+
22
+ for _col, pixel in enumerate(pixel_row):
23
+ self.__draw_pixel(pixel)
24
+ self.__move_pen_next_pixel()
25
+
26
+ self.__screen.update()
27
+
28
+ def load_emulator_window(self):
29
+ if self.loaded:
30
+ return
31
+
32
+ Logger.info("Loading {}".format(self.emulator_details_text()))
33
+ turtle.setup(*self.options.window_size())
34
+ turtle.title(self.emulator_details_text())
35
+ self.__pen = turtle.Turtle(visible=False)
36
+ self.__screen = self.__pen.getscreen()
37
+ self.__set_emulator_icon()
38
+ self.__screen.bgcolor(Color.BLACK())
39
+ turtle.tracer(0, 0)
40
+ turtle.colormode(255)
41
+
42
+ self.loaded = True
43
+
44
+ def __draw_pixel(self, pixel):
45
+ self.__pen.color(*pixel)
46
+ self.__pen.begin_fill()
47
+
48
+ if self.options.pixel_style == "circle":
49
+ self.__draw_circle_pixel()
50
+ else:
51
+ self.__draw_square_pixel()
52
+
53
+ self.__pen.end_fill()
54
+
55
+ self.__pen.setheading(0)
56
+
57
+ def __draw_square_pixel(self):
58
+ for _ in range(0, 4):
59
+ self.__pen.forward(self.options.pixel_size)
60
+ self.__pen.left(90)
61
+
62
+ def __draw_circle_pixel(self):
63
+ self.__pen.pendown()
64
+ self.__pen.dot(self.options.pixel_size)
65
+ self.__pen.penup()
66
+
67
+ # Apparently dots cannot overlap, so set movement to the smallest increment possible
68
+ self.__pen.forward(1)
69
+
70
+ def __move_pen_next_pixel(self):
71
+ self.__pen.penup()
72
+ self.__pen.forward(self.options.pixel_size)
73
+ self.__pen.pendown()
74
+
75
+ def __move_pen_to_row_start(self, row_number):
76
+ self.__reset_pen_position()
77
+ self.__pen.penup()
78
+ self.__pen.setheading(270)
79
+ self.__pen.forward(self.options.pixel_size * row_number)
80
+ self.__pen.setheading(0)
81
+ self.__pen.pendown()
82
+
83
+ def __reset_pen_position(self):
84
+ self.__pen.penup()
85
+ self.__pen.goto(
86
+ self.options.pixel_size / 2 - self.__screen.window_width() / 2,
87
+ self.__screen.window_height() / 2 - self.options.pixel_size / 2,
88
+ )
89
+ self.__pen.pendown()
90
+
91
+ def __set_emulator_icon(self):
92
+ emulator_path = os.path.abspath(os.path.dirname(__file__))
93
+ raw_icon_path = os.path.join(emulator_path, "..", "icon.png")
94
+ icon_path = os.path.normpath(raw_icon_path)
95
+
96
+ icon_image = tkinter.Image("photo", file=icon_path)
97
+ self.__screen._root.iconphoto(True, icon_image)
File without changes
@@ -0,0 +1,71 @@
1
+ import numpy as np
2
+ from PIL import Image, ImageEnhance
3
+ from RGBMatrixEmulator.graphics.color import Color
4
+
5
+
6
+ class Canvas:
7
+ def __init__(self, options):
8
+ self.options = options
9
+
10
+ self.width = options.cols * options.chain_length
11
+ self.height = options.rows * options.parallel
12
+
13
+ # 3D numpy array -- w, h, 3-tuple RGB
14
+ self.__pdims = (self.width, self.height, 3)
15
+
16
+ self.display_adapter = options.display_adapter.get_instance(
17
+ self.width, self.height, options
18
+ )
19
+
20
+ self.Clear()
21
+
22
+ self.display_adapter.load_emulator_window()
23
+
24
+ def Clear(self):
25
+ self.__pixels = np.full(
26
+ self.__pdims, self.__create_pixel(Color.BLACK()), dtype=np.uint8
27
+ )
28
+
29
+ def Fill(self, r, g, b):
30
+ self.__pixels = np.full(
31
+ self.__pdims, self.__create_pixel((r, g, b)), dtype=np.uint8
32
+ )
33
+
34
+ def SetPixel(self, x, y, r, g, b):
35
+ if self.display_adapter.pixel_out_of_bounds(x, y):
36
+ return
37
+
38
+ self.__pixels[int(y)][int(x)] = (r, g, b) * self.brightness
39
+
40
+ def SetImage(self, image, offset_x=0, offset_y=0, *other):
41
+ enhancer = ImageEnhance.Brightness(image)
42
+ image = enhancer.enhance(self.brightness / 100.0)
43
+
44
+ original = Image.fromarray(self.__pixels, "RGB")
45
+ original.paste(image, (offset_x, offset_y))
46
+ self.__pixels = np.copy(original)
47
+
48
+ @property
49
+ def brightness(self):
50
+ return self.options.brightness
51
+
52
+ @brightness.setter
53
+ def brightness(self, value):
54
+ if not isinstance(value, (int, float)):
55
+ raise ValueError(f"brightness must be a numeric value, received '{value}'")
56
+ elif value < 0 or value > 100:
57
+ raise ValueError(
58
+ f"brightness must be a number between 0 and 100, received '{value}'"
59
+ )
60
+
61
+ self.options.brightness = value
62
+
63
+ def __create_pixel(self, pixel):
64
+ return Color.adjust_brightness(tuple(pixel), self.brightness / 100.0)
65
+
66
+ # These are delegated to the display adapter to handle specific implementation.
67
+ def draw_to_screen(self):
68
+ self.display_adapter.draw_to_screen(self.__pixels)
69
+
70
+ def check_for_quit_event(self):
71
+ self.display_adapter.check_for_quit_event()
@@ -0,0 +1,62 @@
1
+ from RGBMatrixEmulator.emulation.canvas import Canvas
2
+
3
+
4
+ class RGBMatrix:
5
+ def __init__(self, options={}):
6
+ self.options = options
7
+
8
+ self.width = options.cols * options.chain_length
9
+ self.height = options.rows * options.parallel
10
+
11
+ self.canvas = None
12
+
13
+ def CreateFrameCanvas(self):
14
+ self.canvas = Canvas(options=self.options)
15
+
16
+ return self.canvas
17
+
18
+ def SwapOnVSync(self, canvas):
19
+ canvas.check_for_quit_event()
20
+ canvas.draw_to_screen()
21
+ self.canvas = canvas
22
+
23
+ return self.canvas
24
+
25
+ def Clear(self):
26
+ self.__sync_canvas()
27
+ self.canvas.Clear()
28
+ self.SwapOnVSync(self.canvas)
29
+
30
+ def Fill(self, r, g, b):
31
+ self.__sync_canvas()
32
+ self.canvas.Fill(r, g, b)
33
+ self.SwapOnVSync(self.canvas)
34
+
35
+ def SetPixel(self, x, y, r, g, b):
36
+ self.__sync_canvas()
37
+ self.canvas.SetPixel(x, y, r, g, b)
38
+ self.SwapOnVSync(self.canvas)
39
+
40
+ def SetImage(self, image, offset_x=0, offset_y=0, *other):
41
+ self.__sync_canvas()
42
+ self.canvas.SetImage(image, offset_x, offset_y, *other)
43
+ self.SwapOnVSync(self.canvas)
44
+
45
+ def __sync_canvas(self):
46
+ if not self.canvas:
47
+ self.canvas = Canvas(options=self.options)
48
+
49
+ @property
50
+ def brightness(self):
51
+ return self.options.brightness
52
+
53
+ @brightness.setter
54
+ def brightness(self, value):
55
+ if not isinstance(value, (int, float)):
56
+ raise ValueError(f"brightness must be a numeric value, received '{value}'")
57
+ elif value < 0 or value > 100:
58
+ raise ValueError(
59
+ f"brightness must be a number between 0 and 100, received '{value}'"
60
+ )
61
+
62
+ self.options.brightness = value