<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="author" content="@ajotanc" /> <title>Pixel Magic Tool</title> <style> :root { --s-thumb: #0006; --s-background: #0003; --overlay: rgba(0, 0, 0, 0.5); --background: #111; --text: #bbb; --gray-dark: #222; --gray-medium: #333; --gray-light: #eeeeee; --blue-dark: #284352; --blue-medium: #3b708b; --blue-light: #48a; --success-dark: #03440c; --success-medium: #548c2f; --success-light: #8ac926; --error-dark: #a80a0a; --error-medium: #c9262b; --error-light: #ff595e; --warning-dark: #dc2f02; --warning-medium: #e85d04; --warning-light: #f48c06; } ::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: var(--s-thumb); opacity: 0.2; border-radius: 5px; } ::-webkit-scrollbar-thumb:hover { background: var(--s-background); } ::selection { background: var(--blue-light); } * { font-family: "Helvetica", Verdana, sans-serif; box-sizing: border-box; margin: 0; padding: 0; } body { margin: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; background: var(--background); } canvas { width: 100%; } small { display: block; font-weight: 400; margin: 2px 0 5px; color: var(--gray-light); font-size: 0.75rem; } footer { display: flex; justify-content: center; margin-block: 20px; } a { text-decoration: none; color: var(--blue-light); font-size: 0.75rem; font-weight: 600; } a:is(:hover, :focus, :active) { color: var(--blue-medium); } #wledEdit { padding: 1.5px 8px; background: var(--blue-light); margin-left: 6px; border-radius: 4px; color: var(--gray-light); } .container { width: 100%; display: flex; align-items: center; justify-content: center; flex-direction: column; } .content { width: min(768px, calc(100% - 40px)); margin-inline: 20px; } .row { display: flex; flex-wrap: nowrap; justify-content: space-between; margin-top: 20px; } .col { flex-basis: calc(50% - 10px); position: relative; padding-inline: 5px; } .col-full { flex-basis: 100%; position: relative; padding-inline: 5px; } .col-small { flex-basis: 33.3333%; position: relative; padding-inline: 5px; } .header { display: flex; flex-direction: column; padding-block: 20px; } .header .title { font-size: 2.5rem; line-height: 2.5rem; font-weight: 800; color: var(--gray-light); padding-bottom: 5px; } .header .subtitle { font-size: 0.75rem; color: var(--text); } .header .rainbow { background: linear-gradient( to right, #ef5350, #f48fb1, #7e57c2, #2196f3, #26c6da, #43a047, #eeff41, #f9a825, #ff5722 ); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-size: 200% 200%; animation: rainbow 5s linear infinite; transition: background-position 0.5s ease; } label { display: flex; margin-bottom: 5px; font-weight: bold; color: var(--text); align-items: center; } input[type="text"], input[type="number"], select, textarea { width: 100%; padding: 10px; border-radius: 50px; background-color: var(--gray-medium); border: 1px solid var(--gray-medium); outline: none; color: var(--gray-light); font-size: 0.875rem; } .input-group { display: flex; justify-content: center; align-items: center; } .input-group input:not([type="range"]) { border-radius: 50px 0 0 50px; } .input-group .input-description { width: 100%; max-width: 38px; height: 38px; display: flex; justify-content: center; align-items: center; color: var(--gray-dark); background: var(--gray-light); border-radius: 0px 50px 50px 0; border: 1px solid var(--gray-light); border-left: 0; font-size: 0.875rem; line-height: 1rem; } .input-group .square { border-radius: 50px !important; margin-left: 10px; } .input-group .square input { text-align: center; background: none; padding: 0; border: 0; color: var(--gray-dark); } textarea { resize: none; border-radius: 8px; } .custom-select select { appearance: none; -webkit-appearance: none; -moz-appearance: none; background-image: none; padding-right: 39px; cursor: pointer; } .custom-select label::after { content: ""; position: absolute; top: calc(50% + 6px); right: 21px; transform: rotate(135deg); width: 6px; height: 6px; border-top: 2px solid var(--gray-light); border-right: 2px solid var(--gray-light); pointer-events: none; } .dropzone { width: 100%; border: 1px dashed var(--gray-light); background-color: var(--gray-dark); color: var(--gray-light); text-align: center; padding: 40px 10px; border-radius: 8px; margin-top: 20px; transition: all 0.5s ease-in-out; } .dropzone:hover { cursor: pointer; color: var(--gray-dark); background-color: var(--gray-light); border-color: var(--gray-dark); } .dropzone.dragover { background-color: var(--gray-medium); } .range-slider { appearance: none; background-color: var(--gray-light); height: 8px; width: 100%; border-radius: 10px; outline: none; margin-block: 15px; } .range-slider::-webkit-slider-thumb, .range-slider::-moz-range-thumb { appearance: none; height: 16px; width: 16px; background-color: var(--blue-light); border-radius: 50%; cursor: pointer; border: 0; } .switch { position: relative; display: inline-block; width: 38px; height: 20px; } .switch input { outline: none; display: none; } .switch-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: var(--gray-medium); border-radius: 34px; transition: 0.4s; } .switch-slider:before { position: absolute; content: ""; height: 14px; width: 14px; left: 3px; bottom: 3px; background-color: white; border-radius: 50%; transition: 0.4s; } input:checked + .switch-slider { background-color: var(--blue-light); } input:focus + .switch-slider { box-shadow: 0 0 1px var(--blue-light); } input:checked + .switch-slider:before { transform: translateX(18px); } #toast-container { position: fixed; bottom: 20px; right: 20px; z-index: 9999; } .toast { display: flex; align-items: center; width: auto; padding: 6px 12px; margin-top: 10px; border-radius: 8px; transform: translateY(30px); opacity: 0; visibility: hidden; } .toast .toast-body { padding-block: 8px; font-weight: 600; color: var(--text); letter-spacing: 0.5px; } .toast.success { background-color: var(--success-medium); } .toast.error { background-color: var(--error-light); } .toast.warning { background-color: var(--warning-light); } .toast-progress { position: absolute; left: 4px; bottom: 4px; width: calc(100% - 8px); height: 3px; transform: scaleX(0); transform-origin: left; border-radius: 8px; } .toast.success .toast-progress { background: linear-gradient( to right, var(--success-light), var(--success-medium) ); } .toast.error .toast-progress { background: linear-gradient( to right, var(--error-light), var(--error-medium) ); } .toast.warning .toast-progress { background: linear-gradient( to right, var(--warning-light), var(--warning-medium) ); } .carousel { display: flex; height: 100%; width: 100%; cursor: pointer; } .button { width: 100%; border: 0; padding: 10px 18px; border-radius: 50px; color: var(--text); cursor: pointer; margin-bottom: 10px; background: var(--gray-medium); border: 1px solid var(--gray-dark); transition: all 0.5s ease-in-out; font-size: 0.875rem; font-weight: 600; } .button:hover { background: var(--gray-dark); border: 1px solid var(--gray-medium); } .button:last-child { margin-bottom: 0; } .buttons { display: flex; flex-direction: column; width: 100%; } #overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: var(--overlay); display: none; } #overlay.loading::after { content: ""; display: block; position: absolute; top: calc(50% - 19px); left: calc(50% - 19px); width: 26px; height: 26px; border: 6px solid var(--gray-light); border-top-color: var(--gray-dark); border-radius: 50%; animation: spin 1s linear infinite; } #recreatedImage { margin-block: 20px; } .invalid { border: 1px solid var(--error-dark) !important; } .error-message { display: block; color: var(--error-dark); padding-block: 4px; font-weight: 600; font-size: 0.75rem; } @media (max-width: 767px) { .row { flex-wrap: wrap; flex-direction: column; margin: 0; } .col, .col-full, .col-small { flex-basis: 100%; margin-top: 20px; padding: 0; } } @keyframes spin { to { transform: rotate(360deg); } } @keyframes progress { to { transform: scaleX(1); } } @keyframes fadeIn { 5% { opacity: 1; visibility: visible; transform: translateY(0); } 95% { opacity: 1; transform: translateY(0); } } @keyframes rainbow { 0% { background-position: 0% 0; } 100% { background-position: 200% 0; } } </style> </head> <body> <div class="container"> <div class="content"> <form id="formGenerate" novalidate> <div class="header"> <span class="title" >PIXEL <span class="rainbow">MAGIC</span> TOOL</span > <span class="subtitle" >It is a tool that converts any image into code in <strong>JSON WLED</strong> format for <strong>2D Matrix</strong> panels</span > </div> <div class="row"> <div class="col" validate> <label for="hostname">Hostname</label> <input type="text" name="hostname" id="hostname" required /> </div> <div class="col" validate> <label for="name">Preset Name</label> <input type="text" name="name" id="name" value="New Preset" required /> </div> </div> <div class="row"> <div class="col" validate> <div class="custom-select"> <label for="pattern">Pattern</label> <select name="pattern" id="pattern" required> <option value="1" title="['ffffff']">Individual</option> <option value="2" title="[0, 'ffffff']">Index</option> <option value="3" title="[0, 5, 'ffffff']" selected> Range </option> </select> </div> </div> <div class="col" validate> <div class="custom-select"> <label for="output">Output</label> <select name="output" id="output" required> <option value="json" selected>WLED JSON</option> <option value="ha">Home Assistant</option> <option value="curl">CURL</option> </select> </div> </div> </div> <div class="row output" style="display: none"> <div class="col" validate> <label for="device">Device</label> <input type="text" name="device" id="device" required /> </div> <div class="col" validate> <label for="uniqueId">Unique Id</label> <input type="text" name="uniqueId" id="uniqueId" required /> </div> <div class="col" validate> <label for="friendlyName">Friendly Name</label> <input type="text" name="friendlyName" id="friendlyName" required /> </div> </div> <div class="row"> <div class="col" validate> <div class="custom-select"> <label for="segments">Segment Id</label> <select name="segments" id="segments"> <option value="0" data-width="16" data-height="16"> Segment 0 </option> </select> </div> </div> <div class="col" validate> <label for="brightness">Brightness</label> <div class="input-group"> <input type="range" name="brightness" id="brightness" min="0" max="255" value="128" class="range-slider" /> <div class="input-description square"> <input type="text" name="brightnessValue" id="brightnessValue" value="128" /> </div> </div> </div> </div> <div class="row"> <div class="col" validate> <label for="animation">Animation</label> <label class="switch"> <input type="checkbox" name="animation" id="animation" data-parent="animation" /> <span class="switch-slider"></span> </label> </div> <div class="col" validate> <label for="transparentImage">Transparent Image</label> <label class="switch"> <input type="checkbox" name="transparentImage" id="transparentImage" data-parent="transparentImage" /> <span class="switch-slider"></span> </label> </div> <div class="col" validate> <label for="resizeImage">Resize Image</label> <label class="switch"> <input type="checkbox" name="resizeImage" id="resizeImage" data-parent="resizeImage" checked /> <span class="switch-slider"></span> </label> </div> </div> <div class="row resizeImage"> <div class="col" validate> <label for="width">Width</label> <input type="number" name="width" id="width" value="16" /> </div> <div class="col" validate> <label for="height">Height</label> <input type="number" name="height" id="height" value="16" /> </div> </div> <div class="row animation" style="display: none"> <div class="col" validate> <label for="frames">Frames</label> <input type="number" name="frames" id="frames" value="4" /> </div> <div class="col" validate> <label for="duration">Duration</label> <div class="input-group"> <input type="number" name="duration" id="duration" value="0.5" required="required" min="0" step="0.1" inputmode="numeric" /> <div class="input-description"> <span>sec</span> </div> </div> </div> <div class="col" validate> <label for="transition">Transition</label> <div class="input-group"> <input type="number" name="transition" id="transition" value="0.2" required="required" min="0" step="0.1" inputmode="numeric" /> <div class="input-description"> <span>sec</span> </div> </div> </div> </div> <div class="row transparentImage" style="display: none"> <div class="col-full" validate> <label for="color">Choose a color</label> <small> Color that will replace the <strong>transparent pixels</strong> in the image </small> <input type="color" name="color" id="color" value="#00BFFF" /> </div> </div> <div class="row"> <div class="col-full" validate> <div class="custom-select"> <label for="images"> <span>Images upload to WLED</span> <a id="wledEdit" href="http://[wled-ip]/edit" target="_blank"> upload </a> </label> <select name="images" id="images"> <option value="">Select image</option> </select> </div> </div> </div> <div id="dropzone" class="dropzone" validate> <p id="dropzoneLabel"> Drag and drop a file here or click to select a local file </p> <input type="file" name="source" id="source" accept="image/*" style="display: none" /> </div> <div class="row"> <div class="col-full"> <button type="button" class="button" id="btnGenerate"> Generate </button> </div> <div class="col-small" id="gbth" style="display: none"> <button type="button" class="button" onclick="window.location.href = WLED_URL;"> Back </button> </div> </div> </form> <div id="preview" style="display: none"> <div id="recreatedImage"></div> <textarea name="response" id="response" rows="8" readonly="readonly"> </textarea> <div class="buttons"> <div class="row"> <div class="col"> <button type="button" class="button" id="btnCopyToClipboard"> Copy to Clipboard </button> </div> <div class="col"> <button type="button" class="button" id="btnSave">Save</button> </div> <div class="col"> <button type="button" class="button" id="btnDownloadPreset"> Download </button> </div> </div> <div class="row" id="simulate" style="display: none"> <div class="col-full"> <button type="button" class="button" id="btnSimulatePreset"> Simulate </button> </div> </div> </div> </div> </div> <footer> <a href="https://github.com/ajotanc/PixelMagicTool" target="_blank"> Github © Pixel Magic Tool </a> </footer> </div> <div id="toast-container"></div> <div id="overlay"></div> </body> <script> const d = document; const params = new URLSearchParams(window.location.search); const host = params.get("hn") ? params.get("hn") : window.location.host ? window.location.host : "0.0.0.0"; const protocol = window.location.protocol === "file:" ? "http:" : window.location.protocol; let WLED_URL = `${protocol}//${host}`; const hostname = gId("hostname"); hostname.value = host; hostname.addEventListener("blur", async () => { WLED_URL = `${protocol}//${hostname.value}`; await segments(); await images(); hostnameLabel(); }); gId("gbth").style.display = window.location.protocol === "http:" ? "flex" : "none"; let jsonSaveWLED = []; let jsonSendWLED = {}; (async function () { await segments(); await images(); hostnameLabel(); })(); function gId(id) { return d.getElementById(id); } function hostnameLabel() { const link = gId("wledEdit"); link.href = WLED_URL + "/edit"; } async function playlist() { const { value: duration } = gId("duration"); const { value: transition } = gId("transition"); const { value: name } = gId("name"); const urlPreset = `${WLED_URL}/presets.json`; const url = `${WLED_URL}/json`; try { const response = await fetch(urlPreset); const data = await response.json(); const items = Object.keys(data); const ps = items.filter( (key) => typeof data[key] === "object" && !data[key].playlist && data[key].n && data[key].n.startsWith(name) ); const id = items.find( (key) => typeof data[key] === "object" && data[key].playlist && data[key].n && data[key].n === name ) || parseInt(items.pop()) + 1; const body = { psave: parseInt(id), n: name, on: true, o: false, playlist: { ps, dur: Array.from({ length: ps.length }, () => duration * 10), transition: Array.from( { length: ps.length }, () => transition * 10 ), repeat: 0, end: 0, }, }; const options = { method: "POST", body: JSON.stringify(body), }; try { const response = await fetch(url, options); const { success } = await response.json(); if (success) { toast(`Playlist "${name}" save successfully`); } } catch (error) { toast(`Error saving preset: ${error}`, "error"); } } catch (error) { toast(error, "error"); } } async function insert(data, isAnimated = false, delay = 5000) { const urlPreset = `${WLED_URL}/presets.json`; const url = `${WLED_URL}/json`; let requestsCompleted = 0; show(); const promises = data.map(async (item) => { return new Promise((resolve, reject) => { setTimeout(async () => { try { const response = await fetch(urlPreset); const data = await response.json(); const items = Object.keys(data); const id = items.find( (key) => typeof data[key] === "object" && !data[key].playlist && data[key].n === item.n ) || parseInt(items.pop()) + 1; const body = Object.assign(item, { psave: parseInt(id) }); const options = { method: "POST", body: JSON.stringify(body), }; try { const response = await fetch(url, options); const { success } = await response.json(); if (success) { toast(`Preset "${item.n}" save successfully`); window.parent.postMessage("loadPresets", WLED_URL); } } catch (error) { toast(`Error saving preset: ${error}`, "error"); } } catch (error) { toast(error, "error"); } finally { resolve(); } }, delay * requestsCompleted++); }); }); await Promise.all(promises); if (isAnimated) { setTimeout(async () => { await playlist() .then(() => { hide(); }) .finally(() => { hide(); }); }, delay); } else { hide(); } } async function images() { const url = `${WLED_URL}/edit?list=/`; const select = gId("images"); show(); try { const response = await fetch(url); const data = await response.json(); const mimeTypes = ["jpeg", "jpg", "png", "gif"]; const images = data.filter((file) => { const { name } = file; const [filename, mimetype] = name.split("."); file.name = name.replace("/", ""); return mimeTypes.includes(mimetype); }); const options = [{ text: "Select image", value: "" }]; if (images.length > 0) { options.push( ...images.map(({ name }) => ({ text: name, value: `${WLED_URL}/${name}`, })) ); select.innerHTML = ""; options.forEach(({ text, value }) => { const option = new Option(text, value); if (index === 0) { option.selected = true; } select.appendChild(option); }); } } catch (error) { toast(error, "error"); } finally { hide(); } } async function segments() { const select = gId("segments"); const width = gId("width"); const height = gId("height"); show(); try { const url = `${WLED_URL}/json/state`; const response = await fetch(url); const { seg: segments } = await response.json(); const options = [ { text: "Segment Default", value: 0, width: 16, height: 16 }, ]; if (segments) { options.splice(0); options.push( ...segments.map(({ id, n, start, stop, startY, stopY }) => ({ text: n ? n : `Segment ${id}`, value: id, width: stop - start, height: stopY - startY, })) ); } select.innerHTML = ""; options.forEach(({ text, value, width: w, height: h }, index) => { const option = new Option(text, value); option.dataset.width = w; option.dataset.height = h; if (index === 0) { option.selected = true; width.value = w; height.value = h; } select.add(option); }); } catch (error) { toast(error, "error"); } finally { hide(); } } gId("dropzone").addEventListener("dragover", (e) => { e.preventDefault(); }); gId("dropzone").addEventListener("drop", (e) => { e.preventDefault(); source.files = e.dataTransfer.files; const { name } = source.files[0]; gId("dropzoneLabel").textContent = `Image ${name} selected!`; validate(e); }); gId("dropzone").addEventListener("click", () => { source.click(); }); gId("source").addEventListener("change", (e) => { const dropzoneLabel = gId("dropzoneLabel"); const { value } = e.target; if (value) { const { name } = e.target.files[0]; dropzoneLabel.textContent = `Image ${name} selected!`; } else { dropzoneLabel.textContent = "Drag and drop a file here or click to select a file"; } validate(e); }); gId("btnSimulatePreset").addEventListener("click", async () => { const url = `${WLED_URL}/json/state`; const options = { method: "POST", body: JSON.stringify(jsonSendWLED), }; show(); try { const response = await fetch(url, options); const { success } = await response.json(); if (success) { toast("Successfully simulated preset"); } } catch (error) { toast(error, "error"); } finally { hide(); } }); gId("btnSave").addEventListener("click", async () => { const { checked } = gId("animation"); const { value: name } = gId("name"); if (checked) { await insert(jsonSaveWLED, true); } else { jsonSaveWLED.splice(0); jsonSaveWLED.push( Object.assign({}, jsonSendWLED, { n: name, o: false }) ); await insert(jsonSaveWLED); } }); gId("btnCopyToClipboard").addEventListener("click", async () => { const response = gId("response"); response.select(); try { await navigator.clipboard.writeText(response.value); toast("Text copied to clipboard"); } catch (error) { try { await d.execCommand("copy"); toast("Text copied to clipboard"); } catch (error) { toast(error, "error"); } } }); gId("btnDownloadPreset").addEventListener("click", () => { const { value: response } = gId("response"); const { value: output } = gId("output"); const timestamp = new Date().getTime(); const filename = `WLED_${timestamp}`; downloadFile(response, filename, output); }); gId("segments").addEventListener("change", (e) => { const { width, height } = e.target.selectedOptions[0].dataset; gId("width").value = w; gId("height").value = h; }); gId("output").addEventListener("change", (e) => { const { value } = e.target.selectedOptions[0]; d.querySelector(".output").style.display = value === "ha" ? "flex" : "none"; }); gId("brightnessValue").addEventListener("input", (e) => { const { value } = e.target; const bri = value <= 255 ? value : 255; gId("brightness").value = bri; e.target.value = bri; }); gId("brightness").addEventListener("input", (e) => { const { value } = e.target; gId("brightnessValue").value = value; }); gId("images").addEventListener("change", (e) => { const dropzone = gId("dropzone"); const { value } = e.target.selectedOptions[0]; if (!value) { const source = gId("source"); gId("dropzoneLabel").textContent = "Drag and drop a file here or click to select a local file"; source.value = ""; dropzone.style.display = "block"; } else { dropzone.style.display = "none"; } }); gId("transparentImage").addEventListener("change", (e) => { const { checked } = e.target; d.querySelector(".transparentImage").style.display = checked ? "flex" : "none"; }); gId("resizeImage").addEventListener("change", (e) => { const { checked } = e.target; d.querySelector(".resizeImage").style.display = checked ? "flex" : "none"; }); gId("animation").addEventListener("change", (e) => { const animation = d.querySelector(".animation"); const source = gId("source"); const { checked } = e.target; if (checked) { toast( 'If you want all frames in the image, set it to "0"', "warning", 5000 ); source.setAttribute("accept", "image/gif"); animation.style.display = "flex"; } else { source.setAttribute("accept", "image/*"); animation.style.display = "none"; } }); gId("btnGenerate").addEventListener("click", async (event) => { const { checked } = gId("animation"); if (validate(event)) { jsonSaveWLED.splice(0); gId("preview").style.display = "block"; gId("recreatedImage").innerHTML = ""; const simulate = gId("simulate"); if (checked) { simulate.style.display = "none"; await generateAnimation(); } else { simulate.style.display = "flex"; await generate(); } } }); async function createObjectURL(url) { return fetch(url) .then((response) => response.arrayBuffer()) .then((buffer) => { const binaryData = new Uint8Array(buffer); const base64 = btoa(String.fromCharCode(...binaryData)); return `data:image/png;base64,${base64}`; }); } function loadImage(url) { return new Promise((resolve, reject) => { const img = new Image(); img.onload = () => resolve(img); img.onerror = (error) => reject(error); img.src = url; }); } async function generate() { const file = gId("source").files[0]; const { value: images } = gId("images"); const { value: output } = gId("output"); show(); try { const response = gId("response"); const recreatedImage = gId("recreatedImage"); const urlImage = !images ? URL.createObjectURL(file) : await createObjectURL(images); const image = await loadImage(urlImage); const { canvas, bri, id, i } = recreate(image); await new Promise((resolve) => { Object.assign(jsonSendWLED, { on: true, bri, seg: { id, i, }, }); resolve(); }); const jsonData = JSON.stringify(jsonSendWLED); const dataResponse = formatOutput(output, jsonData); response.value = dataResponse; recreatedImage.appendChild(canvas); } catch (error) { toast(error, "error"); } finally { hide(); } } async function generateAnimation() { const file = gId("source").files[0]; const images = gId("images"); const response = gId("response"); const { value: presetName } = gId("name"); const { value: amount } = gId("frames"); const { value: output } = gId("output"); const { value: duration } = gId("duration"); const { text: imageName, value: imageValue } = images.selectedOptions[0]; show(); try { const body = new FormData(); if (!imageValue) { body.append("image", file); } else { const responseImage = await fetch(imageValue); const blob = await responseImage.blob(); const file = new File([blob], imageName, { type: blob.type }); body.append("image", file); } const responseFrames = await fetch( `https://pixelmagictool.vercel.app/api/wled/frames?amount=${amount}`, { method: "POST", body, } ); const { message, frames } = await responseFrames.json(); if (!responseFrames.ok) { toast(message, "error"); return; } jsonSaveWLED = []; const texteares = []; const canvases = []; const delay = duration * 1000; const promises = frames.map(async (frame, index) => { const image = await loadImage(frame); const { canvas, bri, id, i } = recreate(image); const jsonData = { on: true, bri, seg: { id, i }, }; const n = `${presetName} ${index + 1}`; texteares.push(`${n}|${JSON.stringify(jsonData)}`); canvases.push(canvas); jsonSaveWLED.push(Object.assign(jsonData, { n, o: true })); }); await Promise.all(promises); const dataResponse = texteares.map((item) => { const [presetName, json] = item.split("|"); const outputFormatted = formatOutput(output, json); const comment = ["ha", "curl"].includes(output) ? "#" : "//"; return `${comment} ${presetName}\n${outputFormatted}`; }); response.value = dataResponse.join("\n"); toast(message); carousel("recreatedImage", canvases, delay); } catch (error) { toast(error, "error"); } finally { hide(); } } function recreate(image) { const { value: pattern } = gId("pattern"); const { value: segmentId } = gId("segments"); const { value: brightness } = gId("brightness"); const { value: inputWidth } = gId("width"); const { value: inputHeight } = gId("height"); const { checked: resizeImage } = gId("resizeImage"); const resizeWidth = parseInt(inputWidth); const resizeHeight = parseInt(inputHeight); const { width: dataWidth, height: dataHeight } = gId("segments").selectedOptions[0].dataset; const segmentWidth = parseInt(dataWidth); const segmentHeight = parseInt(dataHeight); const imgWidth = image.naturalWidth; const imgHeight = image.naturalHeight; const width = resizeImage ? resizeWidth : imgWidth; const height = resizeImage ? resizeHeight : imgHeight; const overallWidth = resizeImage ? segmentWidth : width; const overallHeight = resizeImage ? segmentHeight : height; const startX = Math.floor((segmentWidth - width) / 2); const startY = Math.floor((segmentHeight - height) / 2); const pixelsRef = 48; const canvasWidth = overallWidth * pixelsRef; const canvasHeight = overallHeight * pixelsRef; const fontSize = canvasWidth <= 768 ? 14 : 18; const colors = []; const { ctx: reference } = createCanvas(width, height); reference.drawImage(image, 0, 0, width, height); const imageData = reference.getImageData(0, 0, width, height); const pixels = imageData.data; const { canvas, ctx } = createCanvas(canvasWidth, canvasHeight); ctx.fillStyle = "#000000"; ctx.fillRect(0, 0, canvasWidth, canvasHeight); for (let h = 0; h < overallHeight; h++) { for (let w = 0; w < overallWidth; w++) { let pixelId = h * overallWidth + w + 1; let coordinateX = w * pixelsRef; let coordinateY = h * pixelsRef; let pixelX = w - startX; let pixelY = h - startY; let hex = "000000"; if ( (resizeImage && pixelX >= 0 && pixelX < width && pixelY >= 0 && pixelY < height) || (!resizeImage && w < width && h < height) ) { let index = resizeImage ? pixelY * width + pixelX : h * width + w; let red = pixels[index * 4]; let green = pixels[index * 4 + 1]; let blue = pixels[index * 4 + 2]; let alpha = pixels[index * 4 + 3]; hex = pixelColor(red, green, blue, alpha); let { r, g, b } = hexToRgb(hex); colors.push(hex); ctx.fillStyle = `rgb(${r}, ${g}, ${b})`; ctx.fillRect(coordinateX, coordinateY, pixelsRef, pixelsRef); } else { colors.push("000000"); } ctx.lineWidth = 1; ctx.strokeStyle = "#111111"; ctx.strokeRect(coordinateX, coordinateY, pixelsRef, pixelsRef); let offsetX = coordinateX + pixelsRef / 2; let offsetY = coordinateY + pixelsRef / 2; ctx.font = `${fontSize}px Arial`; ctx.fillStyle = hex === "000000" ? "#eeeeee" : "#111111"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText(pixelId, offsetX, offsetY); } } switch (pattern) { case "1": i = colors; break; case "2": i = index(colors); break; case "3": i = range(colors); break; } return { canvas, bri: parseInt(brightness), id: parseInt(segmentId), i, }; } function range(colors) { let startIndex = 0; let endIndex = 0; let currentColor = colors[0]; let pattern = []; colors.forEach((color, index) => { if (color !== currentColor) { endIndex = index; const repetitions = endIndex - startIndex; if (repetitions == 1) { pattern.push(currentColor); } else { pattern.push(startIndex, endIndex, currentColor); } startIndex = index; } currentColor = color; }); const lastRepetition = colors.length - startIndex; if (lastRepetition === 1) { pattern.push(currentColor); } else { pattern.push(startIndex, colors.length, currentColor); } return pattern; } function pixelColor(r, g, b, a) { const { checked } = gId("transparentImage"); const { value } = gId("color"); if (a === 0) { if (checked) { return value.replace("#", "").toLowerCase(); } return rgbToHex(255, 255, 255); } return rgbToHex(r, g, b); } function hexToRgb(hex) { hex = hex.replace("#", ""); if (hex.length === 3) { hex = hex.replace(/(.)/g, "$1$1"); } const [r, g, b] = hex .match(/.{2}/g) .map((component) => parseInt(component, 16)); return { r, g, b }; } function rgbToHex(r, g, b) { const hex = ((r << 16) | (g << 8) | b).toString(16); return ("000000" + hex).slice(-6); } function index(colors) { const pattern = []; colors.forEach((led, index) => { pattern.push(index, led); }); return pattern; } function createCanvas(width, height) { const canvas = d.createElement("canvas"); canvas.width = width; canvas.height = height; const ctx = canvas.getContext("2d", { willReadFrequently: true }); ctx.imageSmoothingQuality = "high"; ctx.imageSmoothingEnabled = false; return { canvas, ctx }; } function formatOutput(output, json) { return output === "ha" ? yaml(json) : output === "curl" ? curl(json) : json; } function downloadFile(text, filename, output) { let mimeType; let fileExtension; let response; switch (output) { case "json": mimeType = "application/json"; fileExtension = "json"; break; case "ha": mimeType = "application/x-yaml"; fileExtension = "yaml"; break; case "curl": mimeType = "text/plain"; fileExtension = "txt"; break; } const blob = new Blob([text], { type: mimeType }); const url = URL.createObjectURL(blob); const anchorElement = d.createElement("a"); anchorElement.href = url; anchorElement.download = `${filename}.${fileExtension}`; anchorElement.click(); URL.revokeObjectURL(url); } function yaml(jsonData) { const { value: device } = gId("device"); const { value: friendly_name } = gId("friendlyName"); const { value: unique_id } = gId("uniqueId"); const { value: hostname } = gId("hostname"); if (device) { const yamlData = { switches: { [device]: { friendly_name, unique_id, command_on: `curl -X POST "http://${hostname}/json/state" -d '${jsonData}' -H "Content-Type: application/json"`, command_off: `curl -X POST "http://${hostname}/json/state" -d '{"on":false}' -H "Content-Type: application/json"`, }, }, }; return convertToYaml(yamlData); } } function curl(jsonData) { const { value: hostname } = gId("hostname"); return `curl -X POST "http://${hostname}/json/state" -d '${jsonData}' -H "Content-Type: application/json"`; } function convertToYaml(obj) { function processValue(value, indentationLevel) { if (typeof value === "object" && !Array.isArray(value)) { return processObject(value, indentationLevel + 1); } else { return value; } } function processObject(obj, indentationLevel = 0) { const indent = " ".repeat(indentationLevel); const lines = Object.entries(obj).map(([key, value]) => { if (typeof value === "object" && !Array.isArray(value)) { return `${indent}${key}:\n${processObject( value, indentationLevel + 1 )}`; } else { return `${indent}${key}: ${processValue(value, indentationLevel)}`; } }); return lines.join("\n"); } return processObject(obj); } function toast( message, type = "success", duration = 2000, hideElement = "preview" ) { const hide = gId(hideElement); const toast = d.createElement("div"); const wait = 100; toast.style.animation = "fadeIn"; toast.style.animationDuration = `${duration + wait}ms`; toast.style.animationTimingFunction = "linear"; toast.classList.add("toast", type); const body = d.createElement("span"); body.classList.add("toast-body"); body.textContent = message; toast.appendChild(body); const progress = d.createElement("div"); progress.classList.add("toast-progress"); progress.style.animation = "progress"; progress.style.animationDuration = `${duration + wait}ms`; progress.style.animationDelay = "0s"; toast.appendChild(progress); gId("toast-container").appendChild(toast); setTimeout(() => { toast.style.opacity = 0; setTimeout(() => { toast.remove(); }, wait); }, duration); if (type === "error") { hide.style.display = "none"; } } function carousel(id, images, delay = 3000) { let index = 0; const carousel = d.createElement("div"); carousel.classList.add("carousel"); images.forEach((canvas, i) => { if (i === index) { carousel.appendChild(canvas); } else { canvas.style.display = "none"; carousel.appendChild(canvas); } }); const container = gId(id); container.innerHTML = ""; container.appendChild(carousel); function next() { images[index].style.display = "none"; index++; if (index >= images.length) { index = 0; } images[index].style.display = "block"; loop(); } function previous() { images[index].style.display = "none"; index--; if (index < 0) { index = images.length - 1; } images[index].style.display = "block"; loop(); } let timeoutId; function loop() { clearTimeout(timeoutId); timeoutId = setTimeout(() => { next(); if (index === 0) { carousel.scrollTo(0, 0); } }, delay); } loop(); carousel.addEventListener("mouseover", () => { clearTimeout(timeoutId); }); carousel.addEventListener("mouseout", () => { loop(); }); } function show() { const overlay = gId("overlay"); overlay.classList.add("loading"); overlay.style.display = "block"; overlay.style.cursor = "not-allowed"; d.body.style.overflow = "hidden"; } function hide() { const overlay = gId("overlay"); overlay.classList.remove("loading"); overlay.style.display = "none"; overlay.style.cursor = "default"; d.body.style.overflow = "auto"; } function validate(event) { event.preventDefault(); const form = gId("formGenerate"); const inputs = form.querySelectorAll("input, select, textarea"); let isValid = true; const browserLanguage = navigator.language || navigator.userLanguage; const messageRequired = browserLanguage === "pt-br" ? "Este campo é obrigatório" : "This field is required"; inputs.forEach((input) => { const parent = input.closest("[validate]"); if (!parent) { return; } let isVisible = true; let tempParent = parent; while (tempParent !== d.body) { const parentStyles = window.getComputedStyle(tempParent); if ( parentStyles.display === "none" || parentStyles.display === "hidden" ) { isVisible = false; break; } tempParent = tempParent.parentNode; } if ( isVisible && (!input.checkValidity() || (input.type === "file" && input.files.length === 0)) ) { input.classList.add("invalid"); const errorMessage = input.dataset.errorMessage || input.validationMessage || messageRequired; let errorElement = parent.querySelector(".error-message"); if (!errorElement) { errorElement = d.createElement("div"); errorElement.classList.add("error-message"); parent.appendChild(errorElement); } errorElement.innerText = errorMessage; isValid = false; } else { input.classList.remove("invalid"); const errorElement = parent.querySelector(".error-message"); if (errorElement) { parent.removeChild(errorElement); } } }); return isValid; } </script> </html>