Merge pull request #33 from nlimper/main

Painter
This commit is contained in:
Nic Limper
2023-05-16 15:39:51 +02:00
committed by GitHub
8 changed files with 567 additions and 8 deletions

View File

@@ -38,6 +38,7 @@
<option value="11">google calendar</option>
<option value="5">firmware update</option>
</select>
<button id="paintbutton"><i>A</i>&#128396;</button>
</p>
<div id="customoptions"></div>
<p>

View File

@@ -92,14 +92,17 @@ input {
border-radius: 0px;
}
input[type=button] {
input[type=button], button {
border: 0px;
padding: 4px 10px;
cursor:pointer;
}
input[type=button]:hover {
input[type=button]:hover,
button:hover {
background-color:#aaaaaa;
}
select {
padding: 4px;
border-radius: 0px;
@@ -128,7 +131,7 @@ select {
#configbox input, #apconfigbox input {
border: solid 1px #666666;
padding: 4px;
/*padding: 4px;*/
}
#configbox label, #apconfigbox label {
@@ -326,6 +329,81 @@ ul.messages li.new {
color: red;
}
#paintbutton {
padding: 1px 3px;
border: 1px solid black;
font-size: 1.3em;
vertical-align: top;
margin-left:12px;
cursor: pointer;
}
#paintbutton:hover {
background-color: #aaaaaa;
}
/* painter */
#canvasdiv {
padding: 5px;
}
#canvasdiv canvas {
border: 1px solid black;
}
#buttonbar {
padding: 5px;
display: flex;
gap: 5px;
}
#buttonbar button,
#layersdiv button {
padding: 1px 2px;
border: 1px solid #cccccc;
background-color: #dddddd;
width: 40px;
}
#buttonbar button {
font-size: 1.2em;
font-weight: bold;
}
#buttonbar .active {
background-color: #ffffff;
cursor: pointer;
}
#buttonbar button:hover,
#layersdiv button:hover {
background-color: #cccccc;
cursor: pointer;
}
#layersdiv {
padding: 0px 5px;
}
#layersdiv>div {
display: flex;
gap: 5px;
margin-bottom: 5px;
}
#layersdiv input,
#layersdiv select {
padding: 2px;
}
#font-select {
width: 150px;
}
#savebar button {
border: solid 1px #666666;
}
@media(max-width: 460px) {
.messages li div, ul.messages li div.date, ul.messages li div.message {

View File

@@ -31,6 +31,7 @@ contentModeOptions[12] = [];
const imageQueue = [];
let isProcessing = false;
let servertimediff = 0;
let paintLoaded = false, paintShow = false;
let socket;
connect();
@@ -343,6 +344,40 @@ $('#apcfgsave').onclick = function () {
$('#apconfigbox').style.display = 'none';
}
$('#paintbutton').onclick = function () {
if (paintShow) {
paintShow = false;
$('#cfgsave').parentNode.style.display = 'block';
contentselected();
} else {
paintShow = true;
$('#cfgsave').parentNode.style.display = 'none';
$('#customoptions').innerHTML = "<div id=\"buttonbar\"></div><div id=\"canvasdiv\"></div><div id=\"layersdiv\"></div><p id=\"savebar\"></p>";
const mac = $('#cfgmac').dataset.mac
const hwtype = $('#tag' + mac).dataset.hwtype;
var [width, height] = displaySizeLookup[hwtype] || [0, 0];
if (height > width) [width, height] = [height, width];
if (paintLoaded) {
startPainter(mac, width, height);
} else {
loadScript('painter.js', function () {
startPainter(mac, width, height);
});
}
}
}
function loadScript(url, callback) {
var script = document.createElement('script');
script.src = url;
script.onload = function () {
if (callback) {
callback();
}
};
document.head.appendChild(script);
}
function contentselected() {
let contentMode = $('#cfgcontent').value;
let extraoptions = contentModeOptions[contentMode];
@@ -352,6 +387,7 @@ function contentselected() {
obj = JSON.parse($('#cfgcontent').dataset.json);
}
if (contentMode) {
$('#paintbutton').style.display = (contentMode == 0 ? 'inline-block' : 'none');
extraoptions.forEach(element => {
var label = document.createElement("label");
label.innerHTML = element;
@@ -366,6 +402,8 @@ function contentselected() {
$('#customoptions').appendChild(p);
});
}
paintShow = false;
$('#cfgsave').parentNode.style.display = 'block';
}
function showMessage(message,iserr) {

View File

@@ -0,0 +1,360 @@
function startPainter(mac, width, height) {
let isDrawing = false;
let lastX = 0;
let lastY = 0;
let color = 'black';
let linewidth = 3;
let cursor = 'auto';
let isAddingText = false;
let layerDiv, intervalId, showCursor, input, textX, textY, font, sizeSelect, isDragging;
var fonts = ['Roboto', 'Open Sans', 'Lato', 'Montserrat', 'PT Sans', 'Barlow Condensed', 'Headland One', 'Sofia Sans Extra Condensed', 'Mynerve', 'Lilita One', 'Passion One', 'Big Shoulders Display'];
loadGoogleFonts(fonts);
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.imageSmoothingEnabled = false;
$("#canvasdiv").appendChild(canvas);
canvas.style.imageRendering = 'pixelated';
canvas.addEventListener('mousedown', startDrawing);
canvas.addEventListener('mouseup', stopDrawing);
canvas.addEventListener('mousemove', draw);
canvas.addEventListener('touchstart', startDrawing, { passive: true });
canvas.addEventListener('touchend', stopDrawing);
canvas.addEventListener('touchmove', draw, { passive: true });
const bgCanvas = document.createElement('canvas');
bgCanvas.width = canvas.width;
bgCanvas.height = canvas.height;
const bgCtx = bgCanvas.getContext('2d');
bgCtx.fillStyle = '#ffffff';
bgCtx.fillRect(0, 0, bgCanvas.width, bgCanvas.height);
const txtButton = document.createElement('button');
txtButton.innerHTML = 'tT';
txtButton.style.fontStyle = 'italic';
txtButton.addEventListener('click', addText);
const blackButton = document.createElement('button');
blackButton.innerHTML = '&#65103;&#128396';
blackButton.style.color = 'black';
blackButton.addEventListener('click', () => {
color = 'black';
linewidth = 3;
cursor = 'url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\'><circle cx=\'2\' cy=\'2\' r=\'2\' opacity=\'0.5\'/></svg>") 2 2, auto';
blackButton.classList.add('active');
redButton.classList.remove('active');
whiteButton.classList.remove('active');
});
blackButton.classList.add('active');
const redButton = document.createElement('button');
redButton.innerHTML = '&#65103;&#128396';
redButton.style.color = 'red';
redButton.addEventListener('click', () => {
color = 'red';
linewidth = 3;
cursor = 'url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\'><circle cx=\'2\' cy=\'2\' r=\'2\' opacity=\'0.5\'/></svg>") 2 2, auto';
blackButton.classList.remove('active');
redButton.classList.add('active');
whiteButton.classList.remove('active');
});
const whiteButton = document.createElement('button');
whiteButton.innerHTML = '&#11044;';
whiteButton.style.color = 'white';
whiteButton.addEventListener('click', () => {
color = 'white';
linewidth = 20;
cursor = 'url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\'><circle cx=\'10\' cy=\'10\' r=\'10\' opacity=\'0.5\'/></svg>") 10 10, auto';
blackButton.classList.remove('active');
redButton.classList.remove('active');
whiteButton.classList.add('active');
});
const clearButton = document.createElement('button');
clearButton.innerHTML = '&#128437;';
clearButton.addEventListener('click', () => {
if (isAddingText) handleFinish(false);
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
});
const uploadButton = document.createElement('button');
uploadButton.innerHTML = 'Upload';
uploadButton.addEventListener('click', () => {
if (isAddingText) handleFinish(true);
const dataURL = canvas.toDataURL('image/jpeg');
const binaryImage = dataURLToBlob(dataURL);
const formData = new FormData();
formData.append('mac', mac);
formData.append('dither', '0');
formData.append('file', binaryImage, 'image.jpg');
const xhr = new XMLHttpRequest();
xhr.open('POST', '/imgupload');
xhr.send(formData);
$('#configbox').style.display = 'none';
});
$("#buttonbar").appendChild(blackButton);
$("#buttonbar").appendChild(redButton);
$("#buttonbar").appendChild(whiteButton);
$("#buttonbar").appendChild(txtButton);
$("#buttonbar").appendChild(clearButton);
$("#savebar").appendChild(uploadButton);
canvas.addEventListener('mouseenter', function () {
if (!isAddingText) {
canvas.style.cursor = cursor;
} else {
canvas.style.cursor = 'move';
}
});
canvas.addEventListener('mouseleave', function () {
canvas.style.cursor = 'auto';
});
function startDrawing(e) {
if (isAddingText) return;
isDrawing = true;
var rect = canvas.getBoundingClientRect();
lastX = e.pageX - rect.left - window.pageXOffset;
lastY = e.pageY - rect.top - window.pageYOffset;
}
function stopDrawing() {
if (isAddingText) return;
isDrawing = false;
}
function draw(e) {
if (isAddingText) return;
if (!isDrawing) return;
var rect = canvas.getBoundingClientRect();
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(e.pageX - rect.left - window.pageXOffset, e.pageY - rect.top - window.pageYOffset);
ctx.strokeStyle = color;
ctx.lineWidth = linewidth;
ctx.lineCap = "round";
ctx.stroke();
lastX = e.pageX - rect.left - window.pageXOffset;
lastY = e.pageY - rect.top - window.pageYOffset;
}
function addText() {
if (isAddingText) {
handleFinish(true);
return;
}
txtButton.classList.add('active');
bgCtx.drawImage(canvas, 0, 0);
const defaultX = 5;
const defaultY = 40;
isDragging = false;
let startX, startY;
showCursor = true;
textX = defaultX;
textY = defaultY;
font = '24px ' + fonts[0];
input = document.createElement('textarea');
input.type = 'text';
input.placeholder = 'Type text here';
input.style.opacity = '0';
input.style.position = 'absolute';
input.style.left = '-200px'
input.addEventListener('input', () => {
drawText(input.value, textX, textY);
});
input.addEventListener('keyup', () => {
input.selectionStart = input.selectionEnd = input.value.length;
});
input.addEventListener('blur', function () {
input.focus();
});
intervalId = setInterval(function () {
showCursor = !showCursor;
drawText(input.value, textX, textY);
}, 300);
canvas.addEventListener('mouseup', handleMouseUp);
canvas.addEventListener('mousedown', handleMouseDown);
canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('touchstart', handleTouchStart, { passive: true });
canvas.addEventListener('touchend', handleTouchEnd);
canvas.addEventListener('touchmove', handleTouchMove, { passive: true });
var sizes = [10,11,12,13,14,16,18,20,24,28,32,36,40,48,56,64,72,84];
const fontSelect = document.createElement('select');
fontSelect.id = 'font-select';
for (var i = 0; i < fonts.length; i++) {
const option = document.createElement('option');
option.value = fonts[i];
option.text = fonts[i];
option.style.fontFamily = fonts[i];
fontSelect.appendChild(option);
}
sizeSelect = document.createElement('select');
sizeSelect.id = 'size-select';
for (var i = 0; i < sizes.length; i++) {
const option = document.createElement('option');
option.value = sizes[i];
option.text = sizes[i] + ' px';
sizeSelect.appendChild(option);
}
function updateFont() {
var selectedFont = fontSelect.value;
var selectedSize = sizeSelect.value;
fontSelect.style.fontFamily = selectedFont;
font = selectedSize + 'px ' + selectedFont;
drawText(input.value, textX, textY);
}
fontSelect.value = fonts[0];
sizeSelect.value = '24';
fontSelect.addEventListener('change', updateFont);
sizeSelect.addEventListener('change', updateFont);
const finishButton = document.createElement('button');
finishButton.innerHTML = '&#10004;';
finishButton.addEventListener('click', clickHandleFinish);
layerDiv = document.createElement('div');
layerDiv.appendChild(input);
layerDiv.appendChild(fontSelect);
layerDiv.appendChild(sizeSelect);
layerDiv.appendChild(finishButton);
$("#layersdiv").appendChild(layerDiv);
input.focus();
isAddingText = true;
//cursor = 'move';
blackButton.innerHTML = 'aA'
redButton.innerHTML = 'aA'
if (color=='white') {
whiteButton.classList.remove('active');
blackButton.classList.add('active');
color='black';
}
}
function handleFinish(apply) {
canvas.removeEventListener('mousedown', handleMouseDown);
canvas.removeEventListener('mouseup', handleMouseUp);
canvas.removeEventListener('mousemove', handleMouseMove);
canvas.removeEventListener('touchstart', handleTouchStart);
canvas.removeEventListener('touchend', handleTouchEnd);
canvas.removeEventListener('touchmove', handleTouchMove);
isAddingText = false;
cursor = 'auto';
layerDiv.remove();
clearInterval(intervalId);
showCursor = false;
if (apply) drawText(input.value, textX, textY);
txtButton.classList.remove('active');
blackButton.innerHTML = '&#65103;&#128396'
redButton.innerHTML = '&#65103;&#128396'
}
function drawText(text, x, y) {
ctx.drawImage(bgCanvas, 0, 0);
ctx.save();
ctx.translate(x, y);
ctx.font = font;
ctx.fillStyle = color;
const lines = text.split('\n');
lines.forEach((line, index) => {
ctx.fillText(line + (showCursor && index === lines.length - 1 ? '|' : ''), 0, index * (sizeSelect.value * 1.1));
});
ctx.restore();
}
function handleMouseDown(e) {
isDragging = true;
startX = textX;
startY = textY;
({ clientX: lastMouseX, clientY: lastMouseY } = e);
}
function handleMouseMove(e) {
if (isDragging) {
const { clientX, clientY } = e;
textX = startX + clientX - lastMouseX;
textY = startY + clientY - lastMouseY;
drawText(input.value, textX, textY);
}
}
function handleTouchStart(e) {
isDragging = true;
startX = textX;
startY = textY;
({ clientX: lastTouchX, clientY: lastTouchY } = e.touches[0]);
}
function handleTouchMove(e) {
if (isDragging) {
const { clientX, clientY } = e.touches[0];
textX = startX + clientX - lastTouchX;
textY = startY + clientY - lastTouchY;
drawText(input.value, textX, textY);
}
}
function handleMouseUp(e) {
isDragging = false;
}
function handleTouchEnd(e) {
isDragging = false;
}
function clickHandleFinish() {
handleFinish(true);
}
}
function loadGoogleFonts(fonts) {
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'https://fonts.googleapis.com/css?family=' + fonts.join('|');
document.head.appendChild(link);
}
function dataURLToBlob(dataURL) {
const byteString = atob(dataURL.split(',')[1]);
const mimeString = dataURL.split(',')[0].split(':')[1].split(';')[0];
const arrayBuffer = new ArrayBuffer(byteString.length);
const uint8Array = new Uint8Array(arrayBuffer);
for (let i = 0; i < byteString.length; i++) {
uint8Array[i] = byteString.charCodeAt(i);
}
return new Blob([arrayBuffer], { type: mimeString });
}
paintLoaded = true;

View File

@@ -107,7 +107,7 @@ void drawNew(uint8_t mac[8], bool buttonPressed, tagRecord *&taginfo) {
case Image:
if (cfgobj["filename"].as<String>() && cfgobj["filename"].as<String>() != "null" && !cfgobj["#fetched"].as<bool>()) {
if (cfgobj["dither"] && cfgobj["dither"].as<bool>() == false) imageParams.dither = false;
if (cfgobj["dither"] && cfgobj["dither"] == "0") imageParams.dither = false;
jpg2buffer(cfgobj["filename"].as<String>(), filename, imageParams);
if (imageParams.hasRed) imageParams.dataType = DATATYPE_IMG_RAW_2BPP;
if (prepareDataAvail(&filename, imageParams.dataType, mac, cfgobj["timetolive"].as<int>())) {

View File

@@ -62,10 +62,31 @@ struct Error {
float b;
};
// Gamma brightness lookup table <https://victornpb.github.io/gamma-table-generator>
// gamma = 1.50 steps = 256 range = 0-255
const uint8_t gamma_lut[256] PROGMEM = {
0, 0, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4,
4, 4, 5, 5, 6, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11,
11, 12, 12, 13, 14, 14, 15, 15, 16, 16, 17, 18, 18, 19, 20, 20,
21, 21, 22, 23, 23, 24, 25, 26, 26, 27, 28, 28, 29, 30, 31, 31,
32, 33, 34, 34, 35, 36, 37, 37, 38, 39, 40, 41, 41, 42, 43, 44,
45, 46, 46, 47, 48, 49, 50, 51, 52, 53, 53, 54, 55, 56, 57, 58,
59, 60, 61, 62, 63, 64, 65, 65, 66, 67, 68, 69, 70, 71, 72, 73,
74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 88, 89, 90,
91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 102, 103, 104, 105, 106, 107,
108, 109, 110, 112, 113, 114, 115, 116, 117, 119, 120, 121, 122, 123, 124, 126,
127, 128, 129, 130, 132, 133, 134, 135, 136, 138, 139, 140, 141, 142, 144, 145,
146, 147, 149, 150, 151, 152, 154, 155, 156, 158, 159, 160, 161, 163, 164, 165,
167, 168, 169, 171, 172, 173, 174, 176, 177, 178, 180, 181, 182, 184, 185, 187,
188, 189, 191, 192, 193, 195, 196, 197, 199, 200, 202, 203, 204, 206, 207, 209,
210, 211, 213, 214, 216, 217, 218, 220, 221, 223, 224, 226, 227, 228, 230, 231,
233, 234, 236, 237, 239, 240, 242, 243, 245, 246, 248, 249, 251, 252, 254, 255,
};
uint32_t colorDistance(const Color &c1, const Color &c2, const Error &e1) {
float r_diff = c1.r + e1.r - c2.r;
float g_diff = c1.g + e1.g - c2.g;
float b_diff = c1.b + e1.b - c2.b;
float r_diff = gamma_lut[c1.r] + e1.r - gamma_lut[c2.r];
float g_diff = gamma_lut[c1.g] + e1.g - gamma_lut[c2.g];
float b_diff = gamma_lut[c1.b] + e1.b - gamma_lut[c2.b];
return round(r_diff * r_diff + g_diff * g_diff + b_diff * b_diff);
}

View File

@@ -365,12 +365,16 @@ void doImageUpload(AsyncWebServerRequest *request, String filename, size_t index
request->_tempFile.close();
if (request->hasParam("mac", true)) {
String dst = request->getParam("mac", true)->value();
bool dither = true;
if (request->hasParam("dither", true)) {
if (request->getParam("dither", true)->value() == "0") dither = false;
}
uint8_t mac[6];
if (sscanf(dst.c_str(), "%02X%02X%02X%02X%02X%02X", &mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5]) == 6) {
tagRecord *taginfo = nullptr;
taginfo = tagRecord::findByMAC(mac);
if (taginfo != nullptr) {
taginfo->modeConfigJson = "{\"filename\":\"" + dst + ".jpg\",\"timetolive\":\"0\"}";
taginfo->modeConfigJson = "{\"filename\":\"" + dst + ".jpg\",\"timetolive\":\"0\",\"dither\":\"" + String(dither) + "\"}";
taginfo->contentMode = 0;
taginfo->nextupdate = 0;
wsSendTaginfo(mac);

View File

@@ -0,0 +1,57 @@
import os
import json
types = {
0x00: "AP_FW_1.54.bin",
0x01: "AP_FW_2.9.bin",
0xF0: "AP_FW_Segmented_UK.bin",
0xFF: "AP_FW_Nodisplay.bin"
}
binpath = "../binaries/"
tocmaxsize = 512
toc = []
output = bytearray(tocmaxsize)
# Read version from main.c
with open("main.c") as file:
lines = file.readlines()
version_line = next(line for line in lines if "version" in line and "uint16_t" in line)
_, version = version_line.split("= 0x")
version = int(version.strip().split(";")[0], 16)
binaries = [file for file in os.listdir(binpath) if file.startswith("AP_FW") and not file.endswith("Pack")]
for file in binaries:
file = file.strip()
type_id = -1
for typeid, typefile in types.items():
if typefile == file:
type_id = typeid
break
if type_id == -1:
raise ValueError("We don't recognize filetype <{}>, sorry...".format(file))
with open(os.path.join(binpath, file), "rb") as binary_file:
binary = binary_file.read()
length = len(binary)
offset = len(output)
subarr = {
'type': type_id,
'version': version,
'name': file,
'offset': offset,
'length': length
}
toc.append(subarr)
output.extend(binary)
jtoc = json.dumps(toc)
tocsize = len(jtoc)
if tocsize > tocmaxsize:
raise ValueError("TOC is too big! Adjust size and try again.")
output[:len(jtoc)] = jtoc.encode()
with open(os.path.join(binpath, "AP_FW_Pack.bin"), "wb") as output_file:
output_file.write(bytes(output))
print(toc)
print("All done.")