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,5 @@
1
+ from RGBMatrixEmulator.graphics import *
2
+ from RGBMatrixEmulator.emulation.matrix import RGBMatrix
3
+ from RGBMatrixEmulator.emulation.options import RGBMatrixOptions
4
+
5
+ from RGBMatrixEmulator.version import __version__
@@ -0,0 +1,72 @@
1
+ import importlib, json
2
+
3
+ from RGBMatrixEmulator.logger import Logger
4
+
5
+ adapters = [
6
+ {
7
+ "path": "RGBMatrixEmulator.adapters.browser_adapter.adapter",
8
+ "class": "BrowserAdapter",
9
+ "type": "browser",
10
+ },
11
+ {
12
+ "path": "RGBMatrixEmulator.adapters.pygame_adapter",
13
+ "class": "PygameAdapter",
14
+ "type": "pygame",
15
+ },
16
+ {
17
+ "path": "RGBMatrixEmulator.adapters.sixel_adapter",
18
+ "class": "SixelAdapter",
19
+ "type": "sixel",
20
+ },
21
+ {
22
+ "path": "RGBMatrixEmulator.adapters.terminal_adapter",
23
+ "class": "TerminalAdapter",
24
+ "type": "terminal",
25
+ },
26
+ {
27
+ "path": "RGBMatrixEmulator.adapters.tkinter_adapter",
28
+ "class": "TkinterAdapter",
29
+ "type": "tkinter",
30
+ },
31
+ {
32
+ "path": "RGBMatrixEmulator.adapters.turtle_adapter",
33
+ "class": "TurtleAdapter",
34
+ "type": "turtle",
35
+ },
36
+ ]
37
+
38
+ ADAPTER_TYPES = {}
39
+
40
+ try:
41
+ with open("emulator_config.json") as config_file:
42
+ suppress_adapter_load_errors = json.load(config_file).get(
43
+ "suppress_adapter_load_errors", False
44
+ )
45
+ except:
46
+ suppress_adapter_load_errors = False
47
+
48
+ for adapter in adapters:
49
+ package_path = adapter.get("path")
50
+ adapter_class = adapter.get("class")
51
+ adapter_name = adapter.get("type")
52
+ try:
53
+ package = importlib.import_module(package_path)
54
+ adapter = getattr(package, adapter_class)
55
+
56
+ ADAPTER_TYPES[adapter_name] = adapter
57
+ except Exception as ex:
58
+ if suppress_adapter_load_errors:
59
+ continue
60
+
61
+ Logger.exception(
62
+ f"""
63
+ Failed to load {adapter_class} for "{adapter_name}" display adapter!
64
+
65
+ If this is not your configured display adapter, the emulator will continue to load.
66
+
67
+ You can suppress this error in the `emulator_config.json` by adding:
68
+
69
+ "suppress_adapter_load_errors": true
70
+
71
+ """
72
+ )
@@ -0,0 +1,109 @@
1
+ import numpy as np
2
+
3
+ from PIL import Image, ImageDraw
4
+ from RGBMatrixEmulator import version
5
+
6
+
7
+ def draw_circle_mask(drawer, x, y, pixel_size, color):
8
+ drawer.ellipse(
9
+ (x, y, x + pixel_size - 1, y + pixel_size - 1),
10
+ fill=color,
11
+ outline=color,
12
+ )
13
+
14
+
15
+ def draw_square_mask(drawer, x, y, pixel_size, color):
16
+ drawer.rectangle(
17
+ (x, y, x + pixel_size, y + pixel_size),
18
+ fill=color,
19
+ outline=color,
20
+ )
21
+
22
+
23
+ class BaseAdapter:
24
+ SUPPORTS_ALTERNATE_PIXEL_STYLE = False
25
+ INSTANCE = None
26
+
27
+ def __init__(self, width, height, options):
28
+ self.width = width
29
+ self.height = height
30
+ self.options = options
31
+ self.__black = Image.new("RGB", self.options.window_size(), "black")
32
+ self.__mask = self.__draw_mask()
33
+ self.loaded = False
34
+
35
+ def __draw_mask(self):
36
+ mask = Image.new("L", self.options.window_size())
37
+ drawer = ImageDraw.Draw(mask)
38
+ pixel_size = self.options.pixel_size
39
+ width, height = self.options.window_size()
40
+ draw_mask_shape = (
41
+ draw_circle_mask
42
+ if self.options.pixel_style == "circle"
43
+ else draw_square_mask
44
+ )
45
+ for y in range(0, height, pixel_size):
46
+ for x in range(0, width, pixel_size):
47
+ draw_mask_shape(drawer, x, y, pixel_size, 255)
48
+
49
+ return mask
50
+
51
+ @classmethod
52
+ def get_instance(cls, *args, **kwargs):
53
+ if cls.INSTANCE is None:
54
+ instance = cls(*args, **kwargs)
55
+ cls.INSTANCE = instance
56
+
57
+ return cls.INSTANCE
58
+
59
+ def pixel_out_of_bounds(self, x, y):
60
+ if x < 0 or x >= self.width:
61
+ return True
62
+
63
+ if y < 0 or y >= self.height:
64
+ return True
65
+
66
+ return False
67
+
68
+ def _get_masked_image(self, pixels):
69
+ image = Image.fromarray(np.array(pixels, dtype=np.uint8), "RGB")
70
+ image = image.resize(self.options.window_size(), Image.NEAREST)
71
+
72
+ return Image.composite(image, self.__black, self.__mask)
73
+
74
+ def emulator_details_text(self):
75
+ details_text = "RGBME v{} - {}x{} Matrix | {}x{} Chain | {}px per LED ({}) | {}"
76
+
77
+ return details_text.format(
78
+ version.__version__,
79
+ self.options.cols,
80
+ self.options.rows,
81
+ self.options.chain_length,
82
+ self.options.parallel,
83
+ self.options.pixel_size,
84
+ self.options.pixel_style.upper(),
85
+ self.__class__.__name__,
86
+ )
87
+
88
+ # This method is required for the pygame adapter but nothing else, so just skip it if not defined.
89
+ def check_for_quit_event(self):
90
+ pass
91
+
92
+ #############################################################
93
+ # These methods must be implemented by BaseAdapter subclasses
94
+ #############################################################
95
+ def load_emulator_window(self):
96
+ """
97
+ Initialize the external dependency as a graphics display.
98
+
99
+ This method is fired when the emulated canvas is initialized.
100
+ """
101
+ raise NotImplementedError
102
+
103
+ def draw_to_screen(self, _pixels):
104
+ """
105
+ Accepts a 2D array of pixels of size height x width.
106
+
107
+ Implements drawing each pixel to the screen via the external dependency loaded in load_emulator_window.
108
+ """
109
+ raise NotImplementedError
@@ -0,0 +1,89 @@
1
+ # `browser` Display Adapter
2
+
3
+ The `browser` adapter operates differently than other `RGBMatrixEmulator` display adapters. Under the hood, the adapter is a full webserver and WebSocket wrapper and includes its own JS client that can render an emulated image from a Python script within the browser.
4
+
5
+ ![browser-adapter](../../../assets/browser-adapter.gif)
6
+
7
+ ## Running the `browser` Display Adapter Server
8
+
9
+ For configuration of the display adapter, check the main [README's configuration section](../../../README.md#configuration-options).
10
+
11
+ Startup of the script remains unchanged, just like with other display adapters. Additional command line flags work as usual.
12
+
13
+ After running your startup script, you should get a notice that your server is starting on your configured port. For the above example:
14
+
15
+ ```
16
+ C:\Users\tyler\development\RGBMatrixEmulator\samples>python runtext.py
17
+ Press CTRL-C to stop sample
18
+ RGBME v0.6.0 - 32x32 Matrix | 1x1 Chain | 16px per LED (SQUARE) | BrowserAdapter
19
+ Starting server...
20
+ Server started and ready to accept requests on http://localhost:8888/
21
+ ```
22
+
23
+ Once this is configured, your server is up and running and ready to accept requests on the configured port.
24
+
25
+ ## Viewing the Emulator
26
+
27
+ ### Via Websocket
28
+
29
+ By default, the server is configured to `http://localhost:8888/`. Simply navigate to this in the web browser of your choice and you should see streaming images once loaded.
30
+
31
+ If you've configured a custom port, you can navigate to the correct URL instead: `http://localhost:$PORT_NUMBER/`.
32
+
33
+ Once a browser is connected to the server, you will see the following message(s) in your terminal window:
34
+
35
+ ```
36
+ WebSocket opened from: ::1
37
+ WebSocket opened from: 127.0.0.1
38
+ ```
39
+
40
+ This indicates a successful connection has occurred.
41
+
42
+ ### Via Static Image
43
+
44
+ :warning: **This functionality is experimental!** :warning:
45
+
46
+ The emulator also exposes static images in a format you configure. By default, the server is configured to expose this endpoint at `http://localhost:8888/image`.
47
+
48
+ This can be used to allow applications to poll the webserver for updated images or where a websocket is not applicable.
49
+
50
+ #### Tydbit Support
51
+
52
+ Tydbit requires `WebP` images to be exposed over a static HTTP endpoint. You can use the browser adapter's static image endpoint to provide cross-functional compatibility to Tydbit boards.
53
+
54
+ An example configuration that works for Tydbit matrices is provided:
55
+
56
+ ```json
57
+ {
58
+ "pixel_outline": 0,
59
+ "pixel_size": 1,
60
+ "pixel_style": "square",
61
+ "display_adapter": "browser",
62
+ "suppress_font_warnings": false,
63
+ "suppress_adapter_load_errors": false,
64
+ "browser": {
65
+ "port": 8888,
66
+ "target_fps": 24,
67
+ "fps_display": false,
68
+ "quality": 70,
69
+ "image_border": true,
70
+ "debug_text": false,
71
+ "image_format": "WebP"
72
+ },
73
+ "log_level": "info"
74
+ }
75
+ ```
76
+
77
+ ## Error Handling
78
+
79
+ Exceptions in emulated Python scripts will cause the server to shut down. Fix the errors in the script before attempting to restart.
80
+
81
+ The Javascript client attempts to handle errors from the browser gracefully. If for some reason the socket is unable to fetch the next frame, it will retry the fetch every 6 seconds up to 10 times (for a total of 1 minute of retries). Once the max retry count is exhausted, it will stop retrying as there is likely another issue occurring.
82
+
83
+ You can view the errors via a console in the browser (which can be opened with hotkey `F12` or right click -> "Inspect").
84
+
85
+ ## Known Issues
86
+
87
+ * A socket connection is never re-established when shutting down the server (either via `SIGINT` or unhandled exception) and restarting it before the client hits max retry count
88
+ * Excess blob references do not get cleaned up correctly in some instances, leading to memory leaks in some browsers
89
+ * Main symptom appears to be a hanging browser when closing the emulator tab
File without changes
@@ -0,0 +1,51 @@
1
+ import io
2
+
3
+ from RGBMatrixEmulator.adapters.base import BaseAdapter
4
+ from RGBMatrixEmulator.adapters.browser_adapter.server import Server
5
+ from RGBMatrixEmulator.logger import Logger
6
+
7
+
8
+ class BrowserAdapter(BaseAdapter):
9
+ SUPPORTS_ALTERNATE_PIXEL_STYLE = True
10
+ IMAGE_FORMATS = {"bmp": "BMP", "jpeg": "JPEG", "png": "PNG", "webp": "WebP"}
11
+
12
+ def __init__(self, width, height, options):
13
+ super().__init__(width, height, options)
14
+ self.__server = None
15
+ self.image = None
16
+ self.default_image_format = "JPEG"
17
+
18
+ image_format = options.browser.image_format
19
+ if image_format.lower() in self.IMAGE_FORMATS:
20
+ self.image_format = self.IMAGE_FORMATS[image_format.lower()]
21
+ else:
22
+ Logger.warning(
23
+ "Invalid browser image format '{}', falling back to '{}'".format(
24
+ image_format, self.default_image_format
25
+ )
26
+ )
27
+ self.image_format = self.IMAGE_FORMATS.get(
28
+ self.default_image_format.lower()
29
+ )
30
+
31
+ def load_emulator_window(self):
32
+ if self.loaded:
33
+ return
34
+
35
+ Logger.info(self.emulator_details_text())
36
+
37
+ self.__server = Server(self)
38
+ self.__server.run()
39
+
40
+ self.loaded = True
41
+
42
+ def draw_to_screen(self, pixels):
43
+ image = self._get_masked_image(pixels)
44
+ with io.BytesIO() as bytesIO:
45
+ image.save(
46
+ bytesIO,
47
+ self.image_format,
48
+ quality=self.options.browser.quality,
49
+ optimize=True,
50
+ )
51
+ self.image = bytesIO.getvalue()
@@ -0,0 +1,7 @@
1
+ from RGBMatrixEmulator.adapters.browser_adapter.request_handlers.main import MainHandler
2
+ from RGBMatrixEmulator.adapters.browser_adapter.request_handlers.image import (
3
+ ImageHandler,
4
+ )
5
+ from RGBMatrixEmulator.adapters.browser_adapter.request_handlers.image_web_socket import (
6
+ ImageWebSocketHandler,
7
+ )
@@ -0,0 +1,12 @@
1
+ import tornado.web
2
+
3
+
4
+ class ImageHandler(tornado.web.RequestHandler):
5
+ def get(self):
6
+ self.set_header(
7
+ "Content-type", "image/{}".format(self.adapter.image_format.lower())
8
+ )
9
+ self.write(self.adapter.image)
10
+
11
+ def register_adapter(adapter):
12
+ ImageHandler.adapter = adapter
@@ -0,0 +1,34 @@
1
+ import tornado.websocket
2
+
3
+ from RGBMatrixEmulator.logger import Logger
4
+
5
+
6
+ class ImageWebSocketHandler(tornado.websocket.WebSocketHandler):
7
+ clients = set()
8
+ adapter = None
9
+
10
+ def check_origin(self, _origin):
11
+ # Allow access from every origin
12
+ return True
13
+
14
+ def open(self):
15
+ ImageWebSocketHandler.clients.add(self)
16
+ Logger.info("WebSocket opened from: " + self.request.remote_ip)
17
+
18
+ def on_message(self, _message):
19
+ if not ImageWebSocketHandler.adapter.image:
20
+ Logger.warning(
21
+ "No image received from {}!".format(
22
+ ImageWebSocketHandler.adapter.__class__.__name__
23
+ )
24
+ )
25
+ return
26
+
27
+ image_bytes = ImageWebSocketHandler.adapter.image
28
+ self.write_message(image_bytes, binary=True)
29
+
30
+ def on_close(self):
31
+ ImageWebSocketHandler.clients.remove(self)
32
+
33
+ def register_adapter(adapter):
34
+ ImageWebSocketHandler.adapter = adapter
@@ -0,0 +1,9 @@
1
+ import tornado.web
2
+
3
+
4
+ class MainHandler(tornado.web.RequestHandler):
5
+ def get(self):
6
+ self.render("./../static/index.html", adapter=MainHandler.adapter)
7
+
8
+ def register_adapter(adapter):
9
+ MainHandler.adapter = adapter
@@ -0,0 +1,86 @@
1
+ import asyncio
2
+ import signal
3
+ import sys
4
+ import threading
5
+ import tornado.web
6
+ import tornado.ioloop
7
+
8
+ from os import path
9
+ from tornado.platform.asyncio import AnyThreadEventLoopPolicy
10
+
11
+ from RGBMatrixEmulator.adapters.browser_adapter.request_handlers import *
12
+ from RGBMatrixEmulator.logger import Logger
13
+
14
+
15
+ asyncio.set_event_loop_policy(AnyThreadEventLoopPolicy())
16
+
17
+
18
+ class Server:
19
+ instance = None
20
+
21
+ # Singleton class for the server.
22
+ # No more than one webserver can be running at a given time.
23
+ class __Singleton:
24
+ def __init__(self, adapter):
25
+ self.adapter = adapter
26
+ self.io_loop = None
27
+ self.listening = False
28
+
29
+ MainHandler.register_adapter(self.adapter)
30
+ ImageWebSocketHandler.register_adapter(self.adapter)
31
+ ImageHandler.register_adapter(self.adapter)
32
+
33
+ script_path = path.dirname(path.realpath(__file__))
34
+ asset_path = path.normpath(script_path + "/static/assets/")
35
+
36
+ self.app = tornado.web.Application(
37
+ [
38
+ (r"/websocket", ImageWebSocketHandler),
39
+ (r"/image", ImageHandler),
40
+ (r"/", MainHandler),
41
+ (
42
+ r"/assets/(.*)",
43
+ tornado.web.StaticFileHandler,
44
+ {"path": asset_path, "default_filename": "client.js"},
45
+ ),
46
+ ]
47
+ )
48
+
49
+ def __init__(self, adapter):
50
+ if not Server.instance:
51
+ Server.instance = Server.__Singleton(adapter)
52
+
53
+ def run(self):
54
+ if not self.instance.listening:
55
+ Logger.info("Starting server...")
56
+
57
+ self.instance.listening = True
58
+ self.instance.app.listen(self.instance.adapter.options.browser.port)
59
+ self.instance.io_loop = tornado.ioloop.IOLoop.current()
60
+ thread = threading.Thread(
61
+ target=self.instance.io_loop.start,
62
+ name="RGBMEServerThread",
63
+ daemon=True,
64
+ )
65
+ self.__initialize_interrupts()
66
+ thread.start()
67
+
68
+ Logger.info(
69
+ "Server started and ready to accept requests on http://localhost:"
70
+ + str(self.instance.adapter.options.browser.port)
71
+ + "/"
72
+ )
73
+
74
+ def __initialize_interrupts(self):
75
+ """
76
+ Add custom signal handling to ensure webserver thread exits appropriately.
77
+
78
+ Not thread-safe, signal handling must happen on the main thread.
79
+ """
80
+ if threading.current_thread() is threading.main_thread():
81
+ signal.signal(signal.SIGINT, self.__kill)
82
+ signal.signal(signal.SIGTERM, self.__kill)
83
+
84
+ def __kill(self, *_args):
85
+ self.instance.io_loop.add_callback(self.instance.io_loop.stop)
86
+ sys.exit()
@@ -0,0 +1,91 @@
1
+ function init() {
2
+ const WS_RETRY_DELAY = 2000;
3
+ const FPS_DEFAULT = 24;
4
+
5
+ let img = document.getElementById("liveImg");
6
+ let fpsText = document.getElementById("fps");
7
+ let fpsTarget = parseInt(document.getElementById("targetFps").value) || FPS_DEFAULT;
8
+
9
+ let requestStartTime = performance.now();
10
+ let startTime = performance.now();
11
+ let time = 0;
12
+ let requestTime = 0;
13
+ let timeSmoothing = 0.9; // larger=more smoothing
14
+ let requestTimeSmoothing = 0.2; // larger=more smoothing
15
+ let targetTime = 1000 / fpsTarget;
16
+
17
+ let socket = generateSocket();
18
+
19
+ function requestImage() {
20
+ requestStartTime = performance.now();
21
+ socket.send('more');
22
+ }
23
+
24
+ function generateSocket() {
25
+ let path = location.pathname;
26
+
27
+ if (path.endsWith("index.html")) {
28
+ path = path.substring(0, path.length - "index.html".length);
29
+ }
30
+
31
+ if(!path.endsWith("/")) {
32
+ path = path + "/";
33
+ }
34
+
35
+ let wsProtocol = (location.protocol === "https:") ? "wss://" : "ws://";
36
+ let ws = new WebSocket(wsProtocol + location.host + path + "websocket");
37
+
38
+ ws.binaryType = 'arraybuffer';
39
+
40
+ ws.onopen = function() {
41
+ console.log("RGBME WebSocket connection established!");
42
+ startTime = performance.now();
43
+ requestImage();
44
+ };
45
+
46
+ ws.onclose = function() {
47
+ // Handle retries by recreating the connection to websocket.
48
+ console.warn(`RGBME WebSocket connection lost. Retrying in ${WS_RETRY_DELAY / 1000}s.`)
49
+ setTimeout(function() {
50
+ // We generate socket with a timeout to make sure server has time to recover.
51
+ socket = generateSocket();
52
+ }, WS_RETRY_DELAY);
53
+ }
54
+
55
+ ws.onerror = function() {
56
+ ws.close();
57
+ }
58
+
59
+ ws.onmessage = function(evt) {
60
+ let arrayBuffer = evt.data;
61
+ let blob = new Blob([new Uint8Array(arrayBuffer)], {type: "image/jpeg"});
62
+ let old_img = img.src.slice()
63
+ img.src = window.URL.createObjectURL(blob);
64
+ window.URL.revokeObjectURL(old_img);
65
+
66
+ let endTime = performance.now();
67
+ let currentTime = endTime - startTime;
68
+ // smooth with moving average
69
+ time = (time * timeSmoothing) + (currentTime * (1.0 - timeSmoothing));
70
+ startTime = endTime;
71
+ let fps = Math.round(1000 / time);
72
+
73
+ if (fpsText) {
74
+ fpsText.textContent = fps;
75
+ }
76
+
77
+ let currentRequestTime = performance.now() - requestStartTime;
78
+ // smooth with moving average
79
+ requestTime = (requestTime * requestTimeSmoothing) + (currentRequestTime * (1.0 - requestTimeSmoothing));
80
+ let timeout = Math.max(0, targetTime - requestTime);
81
+
82
+ setTimeout(requestImage, timeout);
83
+ };
84
+
85
+ return ws;
86
+ }
87
+
88
+ console.log(`TARGET FPS: ${fpsTarget}`);
89
+ };
90
+
91
+ init();
@@ -0,0 +1,25 @@
1
+ body {
2
+ background-color: black;
3
+ color: white;
4
+ font-family: monospace;
5
+ }
6
+
7
+ img#liveImg {
8
+ border: 1px solid gray;
9
+ }
10
+
11
+ img#liveImg.no-border {
12
+ border: none;
13
+ }
14
+
15
+ .emulatorDetailsTableRow {
16
+ display: flex;
17
+ }
18
+
19
+ .tableContainer {
20
+ margin-right: 20px;
21
+ }
22
+
23
+ td {
24
+ padding-right: 10px;
25
+ }