Live Painting Mode
This commit is contained in:
@@ -97,4 +97,6 @@ Apps = [
|
|||||||
# App(guiname="Snake", name="snake", cmd="./snake.py"),
|
# App(guiname="Snake", name="snake", cmd="./snake.py"),
|
||||||
# App(name="gif", cmd="./gif.sh"),
|
# App(name="gif", cmd="./gif.sh"),
|
||||||
# App(name="colormap", cmd="./colormap.py"),
|
# 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 bottle
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import serial
|
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 config
|
||||||
import filters
|
import filters
|
||||||
@@ -27,7 +32,7 @@ class DataSource:
|
|||||||
self.data = initial
|
self.data = initial
|
||||||
self.listeners = []
|
self.listeners = []
|
||||||
|
|
||||||
def getData(self):
|
def getData(self) -> "Frame":
|
||||||
return self.data
|
return self.data
|
||||||
|
|
||||||
def addListener(self, listener):
|
def addListener(self, listener):
|
||||||
@@ -89,7 +94,7 @@ class LogReader(threading.Thread):
|
|||||||
|
|
||||||
class Frame:
|
class Frame:
|
||||||
def __init__(self, buffer, channels=3):
|
def __init__(self, buffer, channels=3):
|
||||||
self.buffer = buffer
|
self.buffer: np.ndarray = buffer
|
||||||
self.created = time.time()
|
self.created = time.time()
|
||||||
self.channels = channels
|
self.channels = channels
|
||||||
|
|
||||||
@@ -223,13 +228,14 @@ class SerialWriter(threading.Thread):
|
|||||||
# App #
|
# App #
|
||||||
########################################################################
|
########################################################################
|
||||||
class App(threading.Thread):
|
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)
|
super().__init__(daemon=True)
|
||||||
# start app
|
# start app
|
||||||
|
self.name = name
|
||||||
args = cmd + [str(config.ScreenX), str(config.ScreenY), param]
|
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.app = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True, cwd=path)
|
||||||
self.last_update = time.time()
|
self.last_update = time.time()
|
||||||
self.cv = threading.Condition()
|
# self.cv = threading.Condition()
|
||||||
self.watchdog = WatchDog(lambda: self.isAppTimedOut(), lambda: self.terminateApp())
|
self.watchdog = WatchDog(lambda: self.isAppTimedOut(), lambda: self.terminateApp())
|
||||||
self.watchdog.start()
|
self.watchdog.start()
|
||||||
self.logreader = LogReader(self)
|
self.logreader = LogReader(self)
|
||||||
@@ -253,8 +259,8 @@ class App(threading.Thread):
|
|||||||
frame = Frame(buffer, channels=channels)
|
frame = Frame(buffer, channels=channels)
|
||||||
self.last_update = time.time()
|
self.last_update = time.time()
|
||||||
self.datasource.pushData(frame)
|
self.datasource.pushData(frame)
|
||||||
except:
|
except Exception as ex:
|
||||||
logging.debug("Exception in App.run")
|
logging.debug(f"Exception in App.run: {ex}")
|
||||||
with self.listener:
|
with self.listener:
|
||||||
self.listener.notify_all()
|
self.listener.notify_all()
|
||||||
self.watchdog.stop()
|
self.watchdog.stop()
|
||||||
@@ -288,6 +294,35 @@ class App(threading.Thread):
|
|||||||
return time.time() - self.last_update > config.NoDataTimeout
|
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 #
|
# Main #
|
||||||
########################################################################
|
########################################################################
|
||||||
@@ -322,7 +357,10 @@ class AppRunner(threading.Thread):
|
|||||||
|
|
||||||
def startApp(self, i, param=""):
|
def startApp(self, i, param=""):
|
||||||
app = config.Apps[i]
|
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.datasource.addListener(self.cv)
|
||||||
newapp.start()
|
newapp.start()
|
||||||
if app.persistent:
|
if app.persistent:
|
||||||
@@ -451,6 +489,75 @@ def apps_running():
|
|||||||
return config.Apps[runner.currentApp].name
|
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("/")
|
@bottle.route("/")
|
||||||
def index():
|
def index():
|
||||||
return bottle.static_file("index.html", root='html')
|
return bottle.static_file("index.html", root='html')
|
||||||
@@ -523,7 +630,7 @@ def main():
|
|||||||
runner.start()
|
runner.start()
|
||||||
|
|
||||||
# runner.setFilter("5_crazy", MakeBrightnessExprFilter("0.5+0.25*sin(x/3)/x"))
|
# 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 #
|
# Shutdown #
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
bottle
|
bottle
|
||||||
|
bottle-websocket
|
||||||
numpy
|
numpy
|
||||||
scipy
|
scipy
|
||||||
pygame
|
pygame
|
||||||
|
Reference in New Issue
Block a user