mirror of
https://codeberg.org/icewind/ptouch-api.git
synced 2026-06-03 10:54:07 +02:00
366 lines
11 KiB
JavaScript
366 lines
11 KiB
JavaScript
const elementsTable = document.getElementById('elements');
|
|
/** @type HTMLCanvasElement preview */
|
|
const preview = document.getElementById('preview');
|
|
const tapeDiv = document.getElementById('tape');
|
|
const renderCtx = preview.getContext("2d");
|
|
const elementsTableBody = elementsTable.getElementsByTagName('tbody')[0];
|
|
const addTextButton = document.getElementById('add-text');
|
|
const addImageButton = document.getElementById('add-image');
|
|
/** @type HTMLInputElement printButton */
|
|
const printButton = document.getElementById('print');
|
|
const widthInput = document.getElementById('canvas-width');
|
|
const widthUnit = document.getElementById('canvas-width-unit');
|
|
const autoHelp = document.getElementById('autoHelp');
|
|
|
|
let tapeWidthMM = 0;
|
|
let defaultFontSize = 24;
|
|
let backgroundColor = "white";
|
|
let textColor = "white";
|
|
|
|
let images = new Map();
|
|
|
|
const nextElementId = () => {
|
|
let rows = elementsTableBody.getElementsByTagName('tr');
|
|
if (rows.length === 0) {
|
|
return 1;
|
|
} else {
|
|
return parseInt(rows[0].dataset.elementId, 10) + 1;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @typedef ElementSpec
|
|
* @type {object}
|
|
* @property {string} tag
|
|
* @property {Object.<string, string>} props
|
|
* @property {Object.<string, string>} attributes
|
|
* @property {Object.<string, any>} data
|
|
* @property {Object.<string, Function>} events
|
|
* @property {string[]} clases
|
|
* @property {(ElementSpec|string)[]} children
|
|
*/
|
|
|
|
/**
|
|
*
|
|
*
|
|
* @param {string|ElementSpec} spec
|
|
* @returns {HTMLElement|Text}
|
|
*/
|
|
const el = (spec) => {
|
|
if (typeof spec === "string") {
|
|
return document.createTextNode(spec);
|
|
}
|
|
let element = document.createElement(spec.tag);
|
|
for (const [key, value] of Object.entries(spec.props || {})) {
|
|
element[key] = value;
|
|
}
|
|
for (const [key, value] of Object.entries(spec.attributes || {})) {
|
|
element.setAttribute(key, value);
|
|
}
|
|
for (const [key, value] of Object.entries(spec.data || {})) {
|
|
element.dataset[key] = value;
|
|
}
|
|
for (const [key, value] of Object.entries(spec.events || {})) {
|
|
element.addEventListener(key, value);
|
|
}
|
|
for (const c of spec.classes || []) {
|
|
element.classList.add(c);
|
|
}
|
|
for (const child of spec.children || []) {
|
|
element.appendChild(el(child));
|
|
}
|
|
return element;
|
|
}
|
|
|
|
const addElementControls = (row) => {
|
|
row.appendChild(el({
|
|
tag: 'td',
|
|
classes: ['size'],
|
|
children: ['Size', {
|
|
tag: 'input',
|
|
events: {input: render},
|
|
props: {
|
|
type: 'number',
|
|
value: defaultFontSize.toString(),
|
|
}
|
|
}]
|
|
}));
|
|
|
|
row.appendChild(el({
|
|
tag: 'td',
|
|
classes: ['pos'],
|
|
children: ['Position', {
|
|
tag: 'select',
|
|
events: {change: render},
|
|
classes: ['pos_base'],
|
|
children: [
|
|
{tag: 'option', props: {value: 'left'}, children: ['Left']},
|
|
{tag: 'option', props: {value: 'center'}, children: ['Center']},
|
|
{tag: 'option', props: {value: 'right'}, children: ['Right']},
|
|
],
|
|
}, {
|
|
tag: 'input',
|
|
events: {input: render},
|
|
classes: ['pos_x'],
|
|
props: {
|
|
type: 'number',
|
|
value: "0",
|
|
}
|
|
}, {
|
|
tag: 'input',
|
|
events: {input: render},
|
|
classes: ['pos_y'],
|
|
props: {
|
|
type: 'number',
|
|
value: "0",
|
|
}
|
|
}]
|
|
}));
|
|
|
|
row.appendChild(el({
|
|
tag: 'td',
|
|
classes: ['delete'],
|
|
children: [{
|
|
tag: 'input',
|
|
events: {click: removeElement},
|
|
props: {
|
|
type: 'button',
|
|
value: 'x',
|
|
}
|
|
}]
|
|
}));
|
|
}
|
|
|
|
const addTextElement = () => {
|
|
let id = nextElementId();
|
|
let row = el({
|
|
tag: 'tr',
|
|
data: {elementId: id, type: 'text'},
|
|
children: [{
|
|
tag: 'td',
|
|
classes: ['text'],
|
|
props: {colSpan: 2},
|
|
children: [{
|
|
tag: 'input',
|
|
events: {input: render},
|
|
props: {
|
|
type: 'text',
|
|
}
|
|
}]
|
|
}]
|
|
});
|
|
|
|
addElementControls(row);
|
|
elementsTableBody.appendChild(row);
|
|
row.querySelector('input').focus();
|
|
};
|
|
|
|
const loadImage = (event) => {
|
|
/** @type HTMLInputElement input */
|
|
const input = event.target;
|
|
const row = event.target.parentElement.parentElement;
|
|
let file = input.files[0];
|
|
if (file.type.startsWith('image')) {
|
|
let image = row.getElementsByTagName('img')[0];
|
|
image.addEventListener('load', render);
|
|
image.src = URL.createObjectURL(file);
|
|
}
|
|
};
|
|
|
|
const addImageElement = () => {
|
|
let id = nextElementId();
|
|
|
|
let imageId = `image-input-${id}`;
|
|
let row = el({
|
|
tag: 'tr',
|
|
data: {elementId: id, type: 'image'},
|
|
children: [{
|
|
tag: 'td',
|
|
classes: ['image'],
|
|
children: [{
|
|
tag: 'input',
|
|
events: {change: loadImage},
|
|
props: {
|
|
id: imageId,
|
|
type: 'file',
|
|
focus: 'focused',
|
|
}
|
|
},{
|
|
tag: 'label',
|
|
events: {change: loadImage},
|
|
children: "Browse...",
|
|
attributes: {
|
|
for: imageId,
|
|
}
|
|
}]
|
|
}, {
|
|
tag: 'td',
|
|
classes: ['preview'],
|
|
children: [{tag: 'img'}]
|
|
}]
|
|
});
|
|
|
|
addElementControls(row);
|
|
elementsTableBody.appendChild(row);
|
|
};
|
|
|
|
const removeElement = (event) => {
|
|
let row = event.target.parentElement.parentElement;
|
|
let id = row.dataset.elementId;
|
|
row.remove();
|
|
images.delete(id);
|
|
render();
|
|
};
|
|
|
|
const getElements = () => {
|
|
let rows = elementsTableBody.getElementsByTagName('tr');
|
|
let elements = [];
|
|
for (let row of rows) {
|
|
let size = parseInt(row.querySelector('.size input').value, 10);
|
|
let x = parseInt(row.querySelector('.pos_x').value, 10);
|
|
let y = parseInt(row.querySelector('.pos_y').value, 10);
|
|
let base = row.querySelector('.pos_base').value;
|
|
if (row.dataset.type === 'text') {
|
|
let text = row.querySelector('.text input').value;
|
|
elements.push({
|
|
type: 'text',
|
|
text, size, x, y, base,
|
|
width: (renderCtx) => {
|
|
renderCtx.font = `${size}px serif`;
|
|
let textSize = renderCtx.measureText(text);
|
|
return textSize.width;
|
|
}
|
|
});
|
|
} else if (row.dataset.type === 'image') {
|
|
let image = row.getElementsByTagName('img')[0];
|
|
let ratio = image.naturalWidth / image.naturalHeight;
|
|
let width = size * ratio;
|
|
elements.push({
|
|
type: 'image',
|
|
image, size, x, y, base,
|
|
width: (_renderCtx) => width,
|
|
});
|
|
}
|
|
}
|
|
return elements;
|
|
}
|
|
|
|
const render = () => {
|
|
let elements = getElements();
|
|
if (widthUnit.value === "auto") {
|
|
let autoWidth = 20;
|
|
for (let element of elements) {
|
|
if (element.base === 'left') {
|
|
autoWidth = Math.max(autoWidth, element.x + element.width(renderCtx));
|
|
}
|
|
}
|
|
preview.width = autoWidth;
|
|
}
|
|
let width = preview.width;
|
|
let height = preview.height;
|
|
renderCtx.filter = '';
|
|
renderCtx.fillStyle = backgroundColor;
|
|
renderCtx.fillRect(0, 0, width, height);
|
|
|
|
renderCtx.fillStyle = textColor;
|
|
for (let element of elements) {
|
|
let width = element.width(renderCtx);
|
|
if (element.type === 'text') {
|
|
renderCtx.filter = '';
|
|
renderCtx.font = `${element.size}px serif`;
|
|
let textSize = renderCtx.measureText(element.text);
|
|
let textHeight = textSize.fontBoundingBoxAscent - textSize.emHeightDescent;
|
|
renderCtx.fillText(element.text, calcX(element.base, element.x, width), element.y + textHeight);
|
|
} else if (element.type === 'image' && element.image.naturalWidth > 0) {
|
|
renderCtx.filter = 'grayscale() saturate(0%) brightness(70%) contrast(1000%)';
|
|
if (textColor === 'white') {
|
|
renderCtx.filter = renderCtx.filter + ' invert()';
|
|
}
|
|
renderCtx.drawImage(element.image, calcX(element.base, element.x, width), element.y, width, element.size);
|
|
}
|
|
}
|
|
}
|
|
|
|
const calcX = (base, x, width) => {
|
|
let imageWidth = preview.width;
|
|
switch (base) {
|
|
case 'left':
|
|
return x;
|
|
case 'right':
|
|
return (imageWidth - width) + x;
|
|
case 'center':
|
|
return ((imageWidth - width) / 2) + x;
|
|
}
|
|
}
|
|
|
|
const resize = () => {
|
|
let width = parseInt(widthInput.value, 10);
|
|
const pxPerMM = preview.height / tapeWidthMM;
|
|
if (widthUnit.value === "mm") {
|
|
preview.width = width * pxPerMM;
|
|
} else {
|
|
preview.width = width;
|
|
}
|
|
|
|
render()
|
|
};
|
|
|
|
const print = () => {
|
|
printButton.disabled = true;
|
|
preview.toBlob(blob => {
|
|
blob.arrayBuffer().then(png => fetch(apiRoot + '/print', {
|
|
method: 'PUT',
|
|
body: png,
|
|
}))
|
|
.then(() => {
|
|
printButton.disabled = false;
|
|
});
|
|
}, 'image/png');
|
|
};
|
|
|
|
addTextButton.addEventListener('click', addTextElement);
|
|
addImageButton.addEventListener('click', addImageElement);
|
|
printButton.addEventListener('click', print);
|
|
widthInput.addEventListener('input', resize);
|
|
|
|
widthUnit.addEventListener('change', () => {
|
|
const pxPerMM = preview.height / tapeWidthMM;
|
|
if (widthUnit.value === "mm") {
|
|
widthInput.value = (preview.width / pxPerMM).toFixed(0);
|
|
widthInput.disabled = null;
|
|
widthInput.hidden = false;
|
|
autoHelp.classList.remove('show');
|
|
} else if (widthUnit.value === "px") {
|
|
widthInput.value = preview.width;
|
|
widthInput.hidden = false;
|
|
widthInput.disabled = null;
|
|
autoHelp.classList.remove('show');
|
|
} else {
|
|
widthInput.disabled = "disabled";
|
|
widthInput.hidden = true;
|
|
autoHelp.classList.add('show');
|
|
}
|
|
});
|
|
widthUnit.dispatchEvent(new Event('change'));
|
|
|
|
const apiRoot = "";
|
|
|
|
fetch(apiRoot + '/status')
|
|
.then(res => res.json())
|
|
/** @param {{pixel_width: number, media_width: number, margin_width: number, tape_color: string, text_color: string}} status */
|
|
.then(status => {
|
|
preview.height = status.pixel_width;
|
|
defaultFontSize = status.pixel_width;
|
|
backgroundColor = status.tape_color;
|
|
textColor = status.text_color;
|
|
tapeWidthMM = status.media_width;
|
|
|
|
const pxPerMM = preview.height / tapeWidthMM;
|
|
const marginPx = status.margin_width * pxPerMM;
|
|
|
|
tapeDiv.style.padding = `${marginPx}px 0`;
|
|
tapeDiv.style.height = `${status.pixel_width}px`;
|
|
|
|
render();
|
|
addTextElement();
|
|
})
|