RGBMatrixEmulator 0.11.4__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 +68 -0
  3. RGBMatrixEmulator/adapters/base.py +111 -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 +53 -0
  7. RGBMatrixEmulator/adapters/browser_adapter/request_handlers/__init__.py +3 -0
  8. RGBMatrixEmulator/adapters/browser_adapter/request_handlers/image.py +10 -0
  9. RGBMatrixEmulator/adapters/browser_adapter/request_handlers/image_web_socket.py +30 -0
  10. RGBMatrixEmulator/adapters/browser_adapter/request_handlers/main.py +9 -0
  11. RGBMatrixEmulator/adapters/browser_adapter/server.py +73 -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 +71 -0
  17. RGBMatrixEmulator/adapters/sixel_adapter.py +72 -0
  18. RGBMatrixEmulator/adapters/terminal_adapter.py +36 -0
  19. RGBMatrixEmulator/adapters/tkinter_adapter.py +83 -0
  20. RGBMatrixEmulator/adapters/turtle_adapter.py +95 -0
  21. RGBMatrixEmulator/emulators/__init__.py +0 -0
  22. RGBMatrixEmulator/emulators/canvas.py +39 -0
  23. RGBMatrixEmulator/emulators/matrix.py +50 -0
  24. RGBMatrixEmulator/emulators/options.py +188 -0
  25. RGBMatrixEmulator/graphics/__init__.py +155 -0
  26. RGBMatrixEmulator/graphics/color.py +36 -0
  27. RGBMatrixEmulator/graphics/font.py +34 -0
  28. RGBMatrixEmulator/icon.ico +0 -0
  29. RGBMatrixEmulator/icon.png +0 -0
  30. RGBMatrixEmulator/logger.py +29 -0
  31. RGBMatrixEmulator/version.py +5 -0
  32. rgbmatrixemulator-0.11.4.data/data/RGBMatrixEmulator/client.js +91 -0
  33. rgbmatrixemulator-0.11.4.data/data/RGBMatrixEmulator/icon.ico +0 -0
  34. rgbmatrixemulator-0.11.4.data/data/RGBMatrixEmulator/icon.png +0 -0
  35. rgbmatrixemulator-0.11.4.data/data/RGBMatrixEmulator/index.html +144 -0
  36. rgbmatrixemulator-0.11.4.data/data/RGBMatrixEmulator/styles.css +25 -0
  37. rgbmatrixemulator-0.11.4.data/data/docs/LICENSE +9 -0
  38. rgbmatrixemulator-0.11.4.data/data/docs/README.md +160 -0
  39. rgbmatrixemulator-0.11.4.dist-info/METADATA +186 -0
  40. rgbmatrixemulator-0.11.4.dist-info/RECORD +43 -0
  41. rgbmatrixemulator-0.11.4.dist-info/WHEEL +5 -0
  42. rgbmatrixemulator-0.11.4.dist-info/licenses/LICENSE +9 -0
@@ -0,0 +1,5 @@
1
+ from RGBMatrixEmulator.graphics import *
2
+ from RGBMatrixEmulator.emulators.matrix import RGBMatrix
3
+ from RGBMatrixEmulator.emulators.options import RGBMatrixOptions
4
+
5
+ from RGBMatrixEmulator.version import __version__
@@ -0,0 +1,68 @@
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("suppress_adapter_load_errors", False)
43
+ except:
44
+ suppress_adapter_load_errors = False
45
+
46
+ for adapter in adapters:
47
+ package_path = adapter.get('path')
48
+ adapter_class = adapter.get('class')
49
+ adapter_name = adapter.get('type')
50
+ try:
51
+ package = importlib.import_module(package_path)
52
+ adapter = getattr(package, adapter_class)
53
+
54
+ ADAPTER_TYPES[adapter_name] = adapter
55
+ except Exception as ex:
56
+ if suppress_adapter_load_errors: continue
57
+
58
+ Logger.exception(f'''
59
+ Failed to load {adapter_class} for "{adapter_name}" display adapter!
60
+
61
+ If this is not your configured display adapter, the emulator will continue to load.
62
+
63
+ You can suppress this error in the `emulator_config.json` by adding:
64
+
65
+ "suppress_adapter_load_errors": true
66
+
67
+ '''
68
+ )
@@ -0,0 +1,111 @@
1
+ import numpy as np
2
+
3
+ from PIL import Image, ImageDraw, ImageEnhance
4
+ from RGBMatrixEmulator import version
5
+ from RGBMatrixEmulator.graphics import Color
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
+ def draw_square_mask(drawer, x, y, pixel_size, color):
15
+ drawer.rectangle(
16
+ (x, y, x + pixel_size, y + pixel_size),
17
+ fill=color,
18
+ outline=color,
19
+ )
20
+
21
+ class BaseAdapter:
22
+
23
+ SUPPORTS_ALTERNATE_PIXEL_STYLE = False
24
+ INSTANCE = None
25
+
26
+ def __init__(self, width, height, options):
27
+ self.width = width
28
+ self.height = height
29
+ self.options = options
30
+ self.__black = Image.new("RGB", self.options.window_size(), "black")
31
+ self.__mask = self.__draw_mask()
32
+ self.loaded = False
33
+
34
+ def __draw_mask(self):
35
+ mask = Image.new("L", self.options.window_size())
36
+ drawer = ImageDraw.Draw(mask)
37
+ pixel_size = self.options.pixel_size
38
+ width, height = self.options.window_size()
39
+ color = int((self.options.brightness * 255) / 100)
40
+ draw_mask_shape = (draw_circle_mask
41
+ if self.options.pixel_style == "circle" else draw_square_mask)
42
+ for y in range(0, height, pixel_size):
43
+ for x in range(0, width, pixel_size):
44
+ draw_mask_shape(drawer, x, y, pixel_size, color)
45
+
46
+ return mask
47
+
48
+ @classmethod
49
+ def get_instance(cls, *args, **kwargs):
50
+ if cls.INSTANCE is None:
51
+ instance = cls(*args, **kwargs)
52
+ cls.INSTANCE = instance
53
+
54
+ return cls.INSTANCE
55
+
56
+ def adjust_pixel_brightness(self, pixel, to_int = True):
57
+ alpha = self.options.brightness / 100.0
58
+ return Color.adjust_brightness(pixel, alpha, to_int = to_int)
59
+
60
+ def pixel_out_of_bounds(self, x, y):
61
+ if x < 0 or x >= self.width:
62
+ return True
63
+
64
+ if y < 0 or y >= self.height:
65
+ return True
66
+
67
+ return False
68
+
69
+ def _get_masked_image(self, pixels):
70
+ image = Image.fromarray(np.array(pixels, dtype=np.uint8), "RGB")
71
+ image = image.resize(self.options.window_size(), Image.NEAREST)
72
+ enhancer = ImageEnhance.Brightness(image)
73
+ image = enhancer.enhance(self.options.brightness / 100.0)
74
+
75
+ return Image.composite(image, self.__black, self.__mask)
76
+
77
+ def emulator_details_text(self):
78
+ details_text = 'RGBME v{} - {}x{} Matrix | {}x{} Chain | {}px per LED ({}) | {}'
79
+
80
+ return details_text.format(version.__version__,
81
+ self.options.cols,
82
+ self.options.rows,
83
+ self.options.chain_length,
84
+ self.options.parallel,
85
+ self.options.pixel_size,
86
+ self.options.pixel_style.upper(),
87
+ self.__class__.__name__)
88
+
89
+ # This method is required for the pygame adapter but nothing else, so just skip it if not defined.
90
+ def check_for_quit_event(self):
91
+ pass
92
+
93
+ #############################################################
94
+ # These methods must be implemented by BaseAdapter subclasses
95
+ #############################################################
96
+ def load_emulator_window(self):
97
+ '''
98
+ Initialize the external dependency as a graphics display.
99
+
100
+ This method is fired when the emulated canvas is initialized.
101
+ '''
102
+ raise NotImplementedError
103
+
104
+ def draw_to_screen(self, _pixels):
105
+ '''
106
+ Accepts a 2D array of pixels of size height x width.
107
+
108
+ Implements drawing each pixel to the screen via the external dependency loaded in load_emulator_window.
109
+ Before drawing, use adjust_pixel_brightness() on each pixel if your display adapter supports it.
110
+ '''
111
+ 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,53 @@
1
+ import io
2
+
3
+ import numpy as np
4
+
5
+ from PIL import Image, ImageDraw
6
+ from RGBMatrixEmulator.graphics import Color
7
+ from RGBMatrixEmulator.adapters.base import BaseAdapter
8
+ from RGBMatrixEmulator.adapters.browser_adapter.server import Server
9
+ from RGBMatrixEmulator.logger import Logger
10
+
11
+
12
+ class BrowserAdapter(BaseAdapter):
13
+
14
+ SUPPORTS_ALTERNATE_PIXEL_STYLE = True
15
+ IMAGE_FORMATS = {
16
+ 'bmp': 'BMP',
17
+ 'jpeg': 'JPEG',
18
+ 'png': 'PNG',
19
+ 'webp': 'WebP'
20
+ }
21
+
22
+ def __init__(self, width, height, options):
23
+ super().__init__(width, height, options)
24
+ self.__server = None
25
+ self.image = None
26
+ self.default_image_format = "JPEG"
27
+
28
+ image_format = options.browser.image_format
29
+ if image_format.lower() in self.IMAGE_FORMATS:
30
+ self.image_format = self.IMAGE_FORMATS[image_format.lower()]
31
+ else:
32
+ Logger.warning("Invalid browser image format '{}', falling back to '{}'".format(image_format, self.default_image_format))
33
+ self.image_format = self.IMAGE_FORMATS.get(self.default_image_format.lower())
34
+
35
+ def load_emulator_window(self):
36
+ if self.loaded:
37
+ return
38
+
39
+ Logger.info(self.emulator_details_text())
40
+
41
+ self.__server = Server(self)
42
+ self.__server.run()
43
+
44
+ self.loaded = True
45
+
46
+ def draw_to_screen(self, pixels):
47
+ image = self._get_masked_image(pixels)
48
+ with io.BytesIO() as bytesIO:
49
+ image.save(
50
+ bytesIO, self.image_format, quality=self.options.browser.quality, optimize=True
51
+ )
52
+ self.image = bytesIO.getvalue()
53
+
@@ -0,0 +1,3 @@
1
+ from RGBMatrixEmulator.adapters.browser_adapter.request_handlers.main import MainHandler
2
+ from RGBMatrixEmulator.adapters.browser_adapter.request_handlers.image import ImageHandler
3
+ from RGBMatrixEmulator.adapters.browser_adapter.request_handlers.image_web_socket import ImageWebSocketHandler
@@ -0,0 +1,10 @@
1
+ import tornado.web
2
+
3
+
4
+ class ImageHandler(tornado.web.RequestHandler):
5
+ def get(self):
6
+ self.set_header("Content-type", "image/{}".format(self.adapter.image_format.lower()))
7
+ self.write(self.adapter.image)
8
+
9
+ def register_adapter(adapter):
10
+ ImageHandler.adapter = adapter
@@ -0,0 +1,30 @@
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("No image received from {}!".format(ImageWebSocketHandler.adapter.__class__.__name__))
21
+ return
22
+
23
+ image_bytes = ImageWebSocketHandler.adapter.image
24
+ self.write_message(image_bytes, binary=True)
25
+
26
+ def on_close(self):
27
+ ImageWebSocketHandler.clients.remove(self)
28
+
29
+ def register_adapter(adapter):
30
+ 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,73 @@
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
+ class Server:
18
+
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
+
25
+ def __init__(self, adapter):
26
+ self.adapter = adapter
27
+ self.io_loop = None
28
+ self.listening = False
29
+
30
+ MainHandler.register_adapter(self.adapter)
31
+ ImageWebSocketHandler.register_adapter(self.adapter)
32
+ ImageHandler.register_adapter(self.adapter)
33
+
34
+ script_path = path.dirname(path.realpath(__file__))
35
+ asset_path = path.normpath(script_path + '/static/assets/')
36
+
37
+ self.app = tornado.web.Application([
38
+ (r"/websocket", ImageWebSocketHandler),
39
+ (r"/image", ImageHandler),
40
+ (r"/", MainHandler),
41
+ (r"/assets/(.*)", tornado.web.StaticFileHandler, { 'path': asset_path, 'default_filename': 'client.js' })
42
+ ])
43
+
44
+ def __init__(self, adapter):
45
+ if not Server.instance:
46
+ Server.instance = Server.__Singleton(adapter)
47
+
48
+ def run(self):
49
+ if not self.instance.listening:
50
+ Logger.info("Starting server...")
51
+
52
+ self.instance.listening = True
53
+ self.instance.app.listen(self.instance.adapter.options.browser.port)
54
+ self.instance.io_loop = tornado.ioloop.IOLoop.current()
55
+ thread = threading.Thread(target=self.instance.io_loop.start, name="RGBMEServerThread", daemon=True)
56
+ self.__initialize_interrupts()
57
+ thread.start()
58
+
59
+ Logger.info("Server started and ready to accept requests on http://localhost:" + str(self.instance.adapter.options.browser.port) + "/")
60
+
61
+ def __initialize_interrupts(self):
62
+ '''
63
+ Add custom signal handling to ensure webserver thread exits appropriately.
64
+
65
+ Not thread-safe, signal handling must happen on the main thread.
66
+ '''
67
+ if threading.current_thread() is threading.main_thread():
68
+ signal.signal(signal.SIGINT , self.__kill)
69
+ signal.signal(signal.SIGTERM, self.__kill)
70
+
71
+ def __kill(self, *_args):
72
+ self.instance.io_loop.add_callback(self.instance.io_loop.stop)
73
+ 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
+ }