Live Painting Mode
This commit is contained in:
		| @@ -97,4 +97,6 @@ Apps = [ | ||||
|     # App(guiname="Snake", name="snake", cmd="./snake.py"), | ||||
|     # App(name="gif", cmd="./gif.sh"), | ||||
|     # App(name="colormap", cmd="./colormap.py"), | ||||
|  | ||||
|     AppConfig(guiname="Pixel Canvas", name="pixelcanvas", cmd=""), | ||||
| ] | ||||
|   | ||||
							
								
								
									
										465
									
								
								html/draw.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										465
									
								
								html/draw.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,465 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|     <meta charset="UTF-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||
|     <title>Pixel Art Canvas</title> | ||||
|     <style> | ||||
|         /* --- Reset and Base Styles --- */ | ||||
|         :root { | ||||
|             --bg-color: #f7fafc; | ||||
|             --text-color: #2d3748; | ||||
|             --grid-border: #cbd5e0; | ||||
|             --header-color: #1a202c; | ||||
|             --subheader-color: #718096; | ||||
|             --toolbar-bg: #ffffff; | ||||
|             --button-bg: #e53e3e; | ||||
|             --button-hover-bg: #c53030; | ||||
|             --swatch-border: #e2e8f0; | ||||
|             --active-swatch-border: #3b82f6; | ||||
|             --input-bg: #f7fafc; | ||||
|             --input-border: #e2e8f0; | ||||
|             --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | ||||
|             --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); | ||||
|         } | ||||
|  | ||||
|         /* --- Dark Mode --- */ | ||||
|         @media (prefers-color-scheme: dark) { | ||||
|             :root { | ||||
|                 --bg-color: #1a202c; | ||||
|                 --text-color: #e2e8f0; | ||||
|                 --grid-border: #4a5568; | ||||
|                 --header-color: #ffffff; | ||||
|                 --subheader-color: #a0aec0; | ||||
|                 --toolbar-bg: #2d3748; | ||||
|                 --swatch-border: #4a5568; | ||||
|                 --input-bg: #4a5568; | ||||
|                 --input-border: #718096; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         body { | ||||
|             margin: 0; | ||||
|             font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; | ||||
|             background-color: var(--bg-color); | ||||
|             color: var(--text-color); | ||||
|             display: flex; | ||||
|             flex-direction: column; | ||||
|             align-items: center; | ||||
|             justify-content: center; | ||||
|             min-height: 100vh; | ||||
|             padding: 1rem; | ||||
|             box-sizing: border-box; | ||||
|             touch-action: none; /* Disable browser default touch actions like pan/zoom */ | ||||
|         } | ||||
|  | ||||
|         /* --- Layout and Components --- */ | ||||
|         .main-container { | ||||
|             width: 100%; | ||||
|             max-width: 1200px; | ||||
|             display: flex; | ||||
|             flex-direction: column; | ||||
|             align-items: center; | ||||
|         } | ||||
|  | ||||
|         header { | ||||
|             text-align: center; | ||||
|             margin-bottom: 1.5rem; | ||||
|         } | ||||
|  | ||||
|         h1 { | ||||
|             font-size: 2.25rem; | ||||
|             font-weight: 700; | ||||
|             color: var(--header-color); | ||||
|             margin: 0; | ||||
|         } | ||||
|  | ||||
|         header p { | ||||
|             margin-top: 0.25rem; | ||||
|             color: var(--subheader-color); | ||||
|         } | ||||
|  | ||||
|         .toolbar { | ||||
|             display: flex; | ||||
|             flex-wrap: wrap; | ||||
|             align-items: center; | ||||
|             justify-content: center; | ||||
|             gap: 1.5rem; | ||||
|             margin-bottom: 1.5rem; | ||||
|             padding: 1rem; | ||||
|             background-color: var(--toolbar-bg); | ||||
|             border-radius: 0.5rem; | ||||
|             box-shadow: var(--shadow-md); | ||||
|         } | ||||
|  | ||||
|         .control-group { | ||||
|             display: flex; | ||||
|             align-items: center; | ||||
|             gap: 0.75rem; | ||||
|         } | ||||
|  | ||||
|         label { | ||||
|             font-weight: 500; | ||||
|         } | ||||
|  | ||||
|         #colorPicker { | ||||
|             width: 2.5rem; | ||||
|             height: 2.5rem; | ||||
|             padding: 0; | ||||
|             border: none; | ||||
|             border-radius: 0.375rem; | ||||
|             background: none; | ||||
|             cursor: pointer; | ||||
|         } | ||||
|  | ||||
|         #colorPicker::-webkit-color-swatch-wrapper { | ||||
|             padding: 0; | ||||
|         } | ||||
|  | ||||
|         #colorPicker::-webkit-color-swatch { | ||||
|             border: 1px solid var(--swatch-border); | ||||
|             border-radius: 0.375rem; | ||||
|         } | ||||
|  | ||||
|         .color-palette { | ||||
|             display: flex; | ||||
|             gap: 0.5rem; | ||||
|             flex-wrap: wrap; | ||||
|         } | ||||
|  | ||||
|         .color-swatch { | ||||
|             width: 2rem; | ||||
|             height: 2rem; | ||||
|             border-radius: 50%; | ||||
|             border: 2px solid var(--swatch-border); | ||||
|             cursor: pointer; | ||||
|             transition: transform 0.1s ease, border-color 0.1s ease; | ||||
|         } | ||||
|  | ||||
|         .color-swatch:hover { | ||||
|             transform: scale(1.1); | ||||
|         } | ||||
|  | ||||
|         .color-swatch.active { | ||||
|             border-color: var(--active-swatch-border); | ||||
|             transform: scale(1.15); | ||||
|             box-shadow: 0 0 0 2px var(--active-swatch-border); | ||||
|         } | ||||
|  | ||||
|         select { | ||||
|             padding: 0.5rem; | ||||
|             border-radius: 0.375rem; | ||||
|             border: 1px solid var(--input-border); | ||||
|             background-color: var(--input-bg); | ||||
|             color: var(--text-color); | ||||
|         } | ||||
|  | ||||
|         #clearCanvas { | ||||
|             padding: 0.6rem 1rem; | ||||
|             background-color: var(--button-bg); | ||||
|             color: white; | ||||
|             border: none; | ||||
|             border-radius: 0.5rem; | ||||
|             font-weight: 500; | ||||
|             cursor: pointer; | ||||
|             transition: background-color 0.2s ease; | ||||
|         } | ||||
|  | ||||
|         #clearCanvas:hover { | ||||
|             background-color: var(--button-hover-bg); | ||||
|         } | ||||
|  | ||||
|         #grid-container { | ||||
|             display: grid; | ||||
|             grid-template-columns: repeat(80, 1fr); | ||||
|             border: 1px solid var(--grid-border); | ||||
|             width: 100%; | ||||
|             box-shadow: var(--shadow-lg); | ||||
|             cursor: crosshair; | ||||
|         } | ||||
|  | ||||
|         .pixel { | ||||
|             width: 100%; | ||||
|             padding-bottom: 100%; /* Creates a square aspect ratio */ | ||||
|             background-color: #ffffff; | ||||
|             transition: background-color 0.1s ease; | ||||
|         } | ||||
|     </style> | ||||
| </head> | ||||
| <body> | ||||
|  | ||||
| <div class="main-container"> | ||||
|     <header> | ||||
|         <h1>Pixel Art Canvas</h1> | ||||
|         <p>Click and drag to paint. Your changes are live.</p> | ||||
|     </header> | ||||
|  | ||||
|     <div class="toolbar"> | ||||
|         <div class="control-group"> | ||||
|             <label for="colorPicker">Color:</label> | ||||
|             <div id="color-palette" class="color-palette"></div> | ||||
|             <input type="color" id="colorPicker" value="#3b82f6"> | ||||
|         </div> | ||||
|         <div class="control-group"> | ||||
|             <label for="brushSize">Brush Size:</label> | ||||
|             <select id="brushSize"> | ||||
|                 <option value="1">1x1</option> | ||||
|                 <option value="3">3x3</option> | ||||
|                 <option value="5">5x5</option> | ||||
|             </select> | ||||
|         </div> | ||||
|         <div class="control-group"> | ||||
|             <button id="clearCanvas">Clear</button> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <div id="grid-container"></div> | ||||
| </div> | ||||
|  | ||||
| <script> | ||||
|     document.addEventListener('DOMContentLoaded', () => { | ||||
|         // --- DOM Elements --- | ||||
|         const gridContainer = document.getElementById('grid-container'); | ||||
|         const colorPicker = document.getElementById('colorPicker'); | ||||
|         const paletteContainer = document.getElementById('color-palette'); | ||||
|         const brushSizeSelect = document.getElementById('brushSize'); | ||||
|         const clearCanvasBtn = document.getElementById('clearCanvas'); | ||||
|  | ||||
|         // --- Grid Configuration --- | ||||
|         const GRID_ROWS = 40; | ||||
|         const GRID_COLS = 80; | ||||
|         const DEFAULT_COLOR = '#ffffff'; | ||||
|         const PRESET_COLORS = [ | ||||
|             '#ffffff', '#c0c0c0', '#ff0000', '#ffa500', '#ffff00', | ||||
|             '#008000', '#0000ff', '#4b0082', '#ee82ee', '#000000' | ||||
|         ]; | ||||
|  | ||||
|         // --- State Management --- | ||||
|         let pixelData = []; | ||||
|         let isPainting = false; | ||||
|         let currentColor = colorPicker.value; | ||||
|  | ||||
|         // --- Functions --- | ||||
|  | ||||
|         function initializePalette() { | ||||
|             paletteContainer.innerHTML = ''; | ||||
|             PRESET_COLORS.forEach(color => { | ||||
|                 const swatch = document.createElement('div'); | ||||
|                 swatch.classList.add('color-swatch'); | ||||
|                 swatch.style.backgroundColor = color; | ||||
|                 swatch.dataset.color = color; | ||||
|                 paletteContainer.appendChild(swatch); | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         function setActiveColor(color) { | ||||
|             currentColor = color; | ||||
|             colorPicker.value = color; | ||||
|             document.querySelectorAll('.color-swatch').forEach(sw => { | ||||
|                 sw.classList.toggle('active', sw.dataset.color === color); | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         function initializeGrid() { | ||||
|             gridContainer.innerHTML = ''; | ||||
|             pixelData = Array.from({length: GRID_ROWS}, () => Array(GRID_COLS).fill(DEFAULT_COLOR)); | ||||
|             for (let r = 0; r < GRID_ROWS; r++) { | ||||
|                 for (let c = 0; c < GRID_COLS; c++) { | ||||
|                     const pixel = document.createElement('div'); | ||||
|                     pixel.classList.add('pixel'); | ||||
|                     pixel.dataset.row = r; | ||||
|                     pixel.dataset.col = c; | ||||
|                     gridContainer.appendChild(pixel); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * Applies the current brush to the grid centered at the given coordinates. | ||||
|          * @param {number} centerRow - The center row for the brush. | ||||
|          * @param {number} centerCol - The center column for the brush. | ||||
|          */ | ||||
|         function applyBrush(centerRow, centerCol) { | ||||
|             const size = parseInt(brushSizeSelect.value, 10); | ||||
|             const offset = Math.floor((size - 1) / 2); | ||||
|  | ||||
|             for (let r_offset = 0; r_offset < size; r_offset++) { | ||||
|                 for (let c_offset = 0; c_offset < size; c_offset++) { | ||||
|                     const targetRow = centerRow - offset + r_offset; | ||||
|                     const targetCol = centerCol - offset + c_offset; | ||||
|  | ||||
|                     // Check if the target pixel is within the grid bounds | ||||
|                     if (targetRow >= 0 && targetRow < GRID_ROWS && targetCol >= 0 && targetCol < GRID_COLS) { | ||||
|                         if (pixelData[targetRow][targetCol] !== currentColor) { | ||||
|                             pixelData[targetRow][targetCol] = currentColor; | ||||
|  | ||||
|                             // This could be slow on very large brushes, but is fine for this size. | ||||
|                             // A more optimized approach might update the DOM after the loop. | ||||
|                             const pixelEl = gridContainer.querySelector(`[data-row='${targetRow}'][data-col='${targetCol}']`); | ||||
|                             if (pixelEl) { | ||||
|                                 pixelEl.style.backgroundColor = currentColor; | ||||
|                                 const [r, g, b] = hexToRGB(currentColor); | ||||
|                                 if (ws) | ||||
|                                     ws.send(JSON.stringify({ | ||||
|                                         ty: "pixel", | ||||
|                                         x: targetCol, y: targetRow, | ||||
|                                         r, g, b, | ||||
|                                     })); | ||||
|                                 else | ||||
|                                     fetch(`/pixel/${targetCol}/${targetRow}/${r}/${g}/${b}/0`); | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         function rgbToHex(r, g, b, w) { | ||||
|             let hex = ((((r << 8) + g) << 8) + b).toString(16); | ||||
|             while (hex.length < 6) | ||||
|                 hex = "0" + hex; | ||||
|             return "#" + hex; | ||||
|         } | ||||
|  | ||||
|         function hexToRGB(hex) { | ||||
|             const dec = parseInt(hex.substring(1), 16); | ||||
|             const b = dec & 0xff; | ||||
|             const g = (dec >> 8) & 0xff; | ||||
|             const r = (dec >> 16) & 0xff; | ||||
|             return [r, g, b]; | ||||
|         } | ||||
|  | ||||
|         async function loadFrame() { | ||||
|             let data = await (await fetch("/frame")).json(); | ||||
|             setFrame(data); | ||||
|         } | ||||
|  | ||||
|         function setFrame(data) { | ||||
|             for (const pixel of document.getElementsByClassName("pixel")) { | ||||
|                 const row = parseInt(pixel.dataset.row, 10); | ||||
|                 const col = parseInt(pixel.dataset.col, 10); | ||||
|                 const data_base = (row * GRID_COLS + col) * data.channels; | ||||
|                 let color = rgbToHex( | ||||
|                     data.data[data_base], | ||||
|                     data.data[data_base + 1], | ||||
|                     data.data[data_base + 2], | ||||
|                     data.data[data_base + 3]); | ||||
|  | ||||
|                 pixelData[row][col] = color; | ||||
|                 pixel.style.backgroundColor = color; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // --- Event Handlers --- | ||||
|  | ||||
|         function handlePaint(event) { | ||||
|             const targetPixel = event.target.closest('.pixel'); | ||||
|             if (targetPixel) { | ||||
|                 const row = parseInt(targetPixel.dataset.row, 10); | ||||
|                 const col = parseInt(targetPixel.dataset.col, 10); | ||||
|                 applyBrush(row, col); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         function handleTouchPaint(touch) { | ||||
|             const targetPixel = document.elementFromPoint(touch.clientX, touch.clientY); | ||||
|             if (targetPixel && targetPixel.classList.contains('pixel')) { | ||||
|                 const row = parseInt(targetPixel.dataset.row, 10); | ||||
|                 const col = parseInt(targetPixel.dataset.col, 10); | ||||
|                 applyBrush(row, col); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // --- Event Listeners --- | ||||
|  | ||||
|         colorPicker.addEventListener('input', (e) => setActiveColor(e.target.value)); | ||||
|  | ||||
|         paletteContainer.addEventListener('click', (e) => { | ||||
|             const swatch = e.target.closest('.color-swatch'); | ||||
|             if (swatch) setActiveColor(swatch.dataset.color); | ||||
|         }); | ||||
|  | ||||
|         gridContainer.addEventListener('mousedown', (e) => { | ||||
|             e.preventDefault(); | ||||
|             isPainting = true; | ||||
|             handlePaint(e); | ||||
|         }); | ||||
|  | ||||
|         gridContainer.addEventListener('touchstart', (e) => { | ||||
|             e.preventDefault(); | ||||
|             isPainting = true; | ||||
|             if (e.touches.length > 0) handleTouchPaint(e.touches[0]); | ||||
|         }); | ||||
|  | ||||
|         document.addEventListener('mouseup', () => isPainting = false); | ||||
|         document.addEventListener('touchend', () => isPainting = false); | ||||
|         document.addEventListener('touchcancel', () => isPainting = false); | ||||
|  | ||||
|         gridContainer.addEventListener('mousemove', (e) => { | ||||
|             if (isPainting) handlePaint(e); | ||||
|         }); | ||||
|  | ||||
|         gridContainer.addEventListener('touchmove', (e) => { | ||||
|             if (isPainting) { | ||||
|                 e.preventDefault(); // Prevent scrolling while painting | ||||
|                 if (e.touches.length > 0) handleTouchPaint(e.touches[0]); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         clearCanvasBtn.addEventListener('click', () => { | ||||
|             initializeGrid(); | ||||
|             ws.send(JSON.stringify({ | ||||
|                 ty: "fill", | ||||
|                 r: 0, g: 0, b: 0, | ||||
|             })) | ||||
|         }); | ||||
|  | ||||
|         // --- Initial Load --- | ||||
|         initializePalette(); | ||||
|         initializeGrid(); | ||||
|         setActiveColor(colorPicker.value); | ||||
|         // loadFrame(); | ||||
|         // setInterval(loadFrame, 500); | ||||
|  | ||||
|  | ||||
|         let ws = ({ | ||||
|             connect() { | ||||
|                 if (this.ws) | ||||
|                     this.ws.close() | ||||
|  | ||||
|                 this.ws = new WebSocket("/frame_ws"); | ||||
|                 this.ws.onopen = () => { | ||||
|                     clearInterval(this.frame_interval); | ||||
|                     this.frame_interval = setInterval(() => { | ||||
|                         console.log(this); | ||||
|                         this.ws.send(JSON.stringify({ty: "frame", time: this.frame_time, x: 2})) | ||||
|                     }, 100); | ||||
|                 }; | ||||
|                 this.ws.onclose = () => { | ||||
|                     clearInterval(this.frame_interval); | ||||
|                     this.ws = undefined; | ||||
|                 } | ||||
|                 this.ws.onmessage = this.onmessage.bind(this); | ||||
|             }, | ||||
|             onmessage(msg) { | ||||
|                 const data = JSON.parse(msg.data) | ||||
|                 switch (data.ty) { | ||||
|                     case "frame": | ||||
|                         this.frame_time = data.time; | ||||
|                         console.log(this); | ||||
|                         setFrame(data); | ||||
|                         break; | ||||
|                 } | ||||
|             }, | ||||
|             send(data) { | ||||
|                 if (!this.ws) | ||||
|                     this.connect(); | ||||
|                 this.ws.send(data); | ||||
|             }, | ||||
|         }); | ||||
|         ws.connect(); | ||||
|  | ||||
|     }); | ||||
| </script> | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										123
									
								
								main.py
									
									
									
									
									
								
							
							
						
						
									
										123
									
								
								main.py
									
									
									
									
									
								
							| @@ -12,6 +12,11 @@ from collections import OrderedDict | ||||
| import bottle | ||||
| import numpy as np | ||||
| import serial | ||||
| # noinspection PyUnresolvedReferences | ||||
| import bottle.ext.websocket as bottle_ws | ||||
| # noinspection PyUnresolvedReferences | ||||
| from bottle.ext.websocket import GeventWebSocketServer | ||||
| import geventwebsocket.websocket | ||||
|  | ||||
| import config | ||||
| import filters | ||||
| @@ -27,7 +32,7 @@ class DataSource: | ||||
|         self.data = initial | ||||
|         self.listeners = [] | ||||
|  | ||||
|     def getData(self): | ||||
|     def getData(self) -> "Frame": | ||||
|         return self.data | ||||
|  | ||||
|     def addListener(self, listener): | ||||
| @@ -89,7 +94,7 @@ class LogReader(threading.Thread): | ||||
|  | ||||
| class Frame: | ||||
|     def __init__(self, buffer, channels=3): | ||||
|         self.buffer = buffer | ||||
|         self.buffer: np.ndarray = buffer | ||||
|         self.created = time.time() | ||||
|         self.channels = channels | ||||
|  | ||||
| @@ -223,13 +228,14 @@ class SerialWriter(threading.Thread): | ||||
| #                                   App                                # | ||||
| ######################################################################## | ||||
| class App(threading.Thread): | ||||
|     def __init__(self, cmd, param, listener, is_persistent, is_white=False, path="."): | ||||
|     def __init__(self, name, cmd, param, listener, is_persistent, is_white=False, path="."): | ||||
|         super().__init__(daemon=True) | ||||
|         # start app | ||||
|         self.name = name | ||||
|         args = cmd + [str(config.ScreenX), str(config.ScreenY), param] | ||||
|         self.app = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True, cwd=path) | ||||
|         self.last_update = time.time() | ||||
|         self.cv = threading.Condition() | ||||
|         # self.cv = threading.Condition() | ||||
|         self.watchdog = WatchDog(lambda: self.isAppTimedOut(), lambda: self.terminateApp()) | ||||
|         self.watchdog.start() | ||||
|         self.logreader = LogReader(self) | ||||
| @@ -253,8 +259,8 @@ class App(threading.Thread): | ||||
|                 frame = Frame(buffer, channels=channels) | ||||
|                 self.last_update = time.time() | ||||
|                 self.datasource.pushData(frame) | ||||
|             except: | ||||
|                 logging.debug("Exception in App.run") | ||||
|             except Exception as ex: | ||||
|                 logging.debug(f"Exception in App.run: {ex}") | ||||
|         with self.listener: | ||||
|             self.listener.notify_all() | ||||
|         self.watchdog.stop() | ||||
| @@ -288,6 +294,35 @@ class App(threading.Thread): | ||||
|         return time.time() - self.last_update > config.NoDataTimeout | ||||
|  | ||||
|  | ||||
| class PixelCanvas(App): | ||||
|     # noinspection PyMissingConstructor | ||||
|     def __init__(self): | ||||
|         threading.Thread.__init__(self, daemon=True) | ||||
|         self.name = "pixelcanvas" | ||||
|         self.running = True | ||||
|         self.is_persistent = True | ||||
|         self.datasource = DataSource(Frame(np.zeros((config.ScreenY, config.ScreenX, 3)))) | ||||
|  | ||||
|     def run(self): | ||||
|         while running and self.running: | ||||
|             time.sleep(1) | ||||
|  | ||||
|     def alive(self): | ||||
|         return True | ||||
|  | ||||
|     def stop(self): | ||||
|         pass | ||||
|  | ||||
|     def getLog(self): | ||||
|         return "" | ||||
|  | ||||
|     def terminateApp(self): | ||||
|         pass | ||||
|  | ||||
|     def isAppTimedOut(self): | ||||
|         return False | ||||
|  | ||||
|  | ||||
| ######################################################################## | ||||
| #                                  Main                                # | ||||
| ######################################################################## | ||||
| @@ -322,7 +357,10 @@ class AppRunner(threading.Thread): | ||||
|  | ||||
|     def startApp(self, i, param=""): | ||||
|         app = config.Apps[i] | ||||
|         newapp = App(app.cmd, param, self.cv, is_persistent=app.persistent, is_white=app.white, path=app.path) | ||||
|         if app.name == "pixelcanvas": | ||||
|             newapp = PixelCanvas() | ||||
|         else: | ||||
|             newapp = App(app.name, app.cmd, param, self.cv, is_persistent=app.persistent, is_white=app.white, path=app.path) | ||||
|         newapp.datasource.addListener(self.cv) | ||||
|         newapp.start() | ||||
|         if app.persistent: | ||||
| @@ -451,6 +489,75 @@ def apps_running(): | ||||
|     return config.Apps[runner.currentApp].name | ||||
|  | ||||
|  | ||||
| @bottle.route("/frame") | ||||
| def frame(): | ||||
|     data = runner.datasource.getData() | ||||
|     return {"data": data.buffer.flatten().tolist(), "channels": data.channels} | ||||
|  | ||||
|  | ||||
| @bottle.route("/pixel/<x:int>/<y:int>/<r:int>/<g:int>/<b:int>/<w:int>") | ||||
| def pixel(x, y, r, g, b, w): | ||||
|     if runner.app.name != "pixelcanvas": | ||||
|         startApp("pixelcanvas") | ||||
|  | ||||
|     data = runner.app.datasource.getData().clone() | ||||
|     data.created = time.time() | ||||
|     data.buffer[y][x][0] = r | ||||
|     data.buffer[y][x][1] = g | ||||
|     data.buffer[y][x][2] = b | ||||
|     if data.channels == 4: | ||||
|         data.buffer[y][x][3] = w | ||||
|     runner.datasource.pushData(data) | ||||
|     return "ok" | ||||
|  | ||||
|  | ||||
| @bottle.get("/frame_ws", apply=[bottle_ws.websocket]) | ||||
| def frame_ws(ws: geventwebsocket.websocket.WebSocket): | ||||
|     while not ws.closed: | ||||
|         msg = json.loads(ws.receive()) | ||||
|  | ||||
|         if msg["ty"] == "frame": | ||||
|             data = runner.datasource.getData() | ||||
|             if msg.get("time", None) == str(data.created): | ||||
|                 ws.send(json.dumps({ | ||||
|                     "type": "frame_unchanged", | ||||
|                 }, separators=(',', ':'))) | ||||
|             else: | ||||
|                 ws.send(json.dumps({ | ||||
|                     "ty": "frame", | ||||
|                     "data": data.buffer.flatten().tolist(), | ||||
|                     "channels": data.channels, | ||||
|                     "time": str(data.created), | ||||
|                 }, separators=(',', ':'))) | ||||
|         elif msg["ty"] == "pixel": | ||||
|             if runner.app.name != "pixelcanvas": | ||||
|                 startApp("pixelcanvas") | ||||
|  | ||||
|             x = msg["x"] | ||||
|             y = msg["y"] | ||||
|  | ||||
|             data = runner.app.datasource.getData().clone() | ||||
|             data.created = time.time() | ||||
|             data.buffer[y, x, 0] = msg["r"] | ||||
|             data.buffer[y, x, 1] = msg["g"] | ||||
|             data.buffer[y, x, 2] = msg["b"] | ||||
|             if data.channels == 4 and "w" in msg: | ||||
|                 data.buffer[y, x, 3] = msg["w"] | ||||
|             runner.app.datasource.pushData(data) | ||||
|         elif msg["ty"] == "fill": | ||||
|             if runner.app.name != "pixelcanvas": | ||||
|                 startApp("pixelcanvas") | ||||
|  | ||||
|             data = runner.app.datasource.getData().clone() | ||||
|             data.created = time.time() | ||||
|             data.buffer[..., 0] = msg["r"] | ||||
|             data.buffer[..., 1] = msg["g"] | ||||
|             data.buffer[..., 2] = msg["b"] | ||||
|             if data.channels == 4 and "w" in msg: | ||||
|                 data.buffer[..., 3] = msg["w"] | ||||
|             runner.app.datasource.pushData(data) | ||||
|  | ||||
|  | ||||
| @bottle.route("/") | ||||
| def index(): | ||||
|     return bottle.static_file("index.html", root='html') | ||||
| @@ -523,7 +630,7 @@ def main(): | ||||
|     runner.start() | ||||
|  | ||||
|     # runner.setFilter("5_crazy", MakeBrightnessExprFilter("0.5+0.25*sin(x/3)/x")) | ||||
|     bottle.run(host=config.WebHost, port=config.WebPort) | ||||
|     bottle.run(host=config.WebHost, port=config.WebPort, server=GeventWebSocketServer) | ||||
|  | ||||
|     ######################################################################## | ||||
|     #                              Shutdown                                # | ||||
|   | ||||
| @@ -1,7 +1,8 @@ | ||||
| bottle | ||||
| bottle-websocket | ||||
| numpy | ||||
| scipy | ||||
| pygame | ||||
| pyserial | ||||
| paho-mqtt | ||||
| pillow | ||||
| pillow | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 T
					T