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.} props * @property {Object.} attributes * @property {Object.} data * @property {Object.} 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(); })