Intended to minimize load on the WLED device. Small file size, same functionality, absolutely impossible to modify localy (and stay sane)
214 lines
18 KiB
HTML
214 lines
18 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
|
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
|
<meta http-equiv="Pragma" content="no-cache">
|
|
<meta http-equiv="Expires" content="0">
|
|
<title>Led Matrix Pixel Art Convertor</title>
|
|
<style>
|
|
h1,h2{margin:20px 0;line-height:.5;font-family:Arcade,Arial,sans-serif}#drop-zone,#fieldTable,* input[type=text],body,h1,h2,p{font-family:Arcade,Arial,sans-serif}#drop-zone,* select,p{color:#777}.box{border:2px solid #fff}body{background-color:#151515}.top-part{width:600px;margin:0 auto}#drop-zone,.button{display:block;text-align:center;padding:20px;margin:0;cursor:pointer}.container{max-width:100% -40px;border-radius:0;padding:20px;text-align:center}h1{font-size:2.3em;color:#7e4c80;text-align:center}h2{font-size:1.3em;color:rgba(126,76,128,.61);text-align:center}p{font-size:1.2em;line-height:1.5}#fieldTable{font-size:1 em;color:#777;line-height:1}#drop-zone{width:100%-40px;border:3px dashed #7e4c80;border-radius:0;font-size:15px}.button{width:100% - 40px;border:2px dashed #ccc;border-radius:20px}* input[type=text],* select{background-color:#333;border:1px solid silver;width:100%;height:27px;font-size:15px}#file-picker,.hide{display:none}* select{margin-top:.5em;margin-bottom:.5em;padding:0;border-radius:0}* input[type=range]{-webkit-appearance:none;flex-grow:1;border-radius:0;background:linear-gradient(to right,#333 0,#333 100%);color:silver;border:1px solid silver;margin-top:.5em;margin-left:0}input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:25px;height:25px;background:#7e4c80;position:relative;z-index:3}.rangeNumber{width:20px;vertical-align:middle}* input[type=text]{padding-inline-start:5px;margin-top:10px;border-radius:0;color:#777}* button,* input[type=submit]{border:1px solid silver;padding:.5em;font-family:Arcade,Arial,sans-serif;font-size:1.3em;background-color:#333;width:100%;color:#777}* input[type=submit]{border-radius:0}* button{margin-bottom:15px;border-radius:0}* textarea{background-color:#333;border:1px solid silver;padding:0;margin-bottom:10px;width:100%;height:200px;border-radius:0;font-family:Courier,Arial,sans-serif;font-size:1em;color:#777}.gridstyle{position:fixed;top:0;left:0;width:100vw;height:100vh;display:grid;grid-template-columns:repeat(auto-fill,20px);grid-template-rows:repeat(auto-fill,20px);grid-gap:0px}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div class = top-part>
|
|
<h1>Led Matrix Pixel Art Converter</h1>
|
|
<h2>Convert image to WLED JSON (pixel art on WLED matrix)</h2>
|
|
<p>
|
|
<table id="fieldTable" style="width: 100%; table-layout: fixed; align-content: center;">
|
|
<tr>
|
|
<td style="vertical-align: middle;">
|
|
<label for="ledSetupSelector">Led setup:</label>
|
|
</td>
|
|
<td style="vertical-align: middle;">
|
|
<select id="ledSetupSelector">
|
|
<option value="matrix" selected>2D Matrix</option>
|
|
<option value="r2l">Serpentine, first row right to left <-</option>
|
|
<option value="l2r">Serpentine, first row left to right -></option>
|
|
</select>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="vertical-align: middle;">
|
|
<label for="formatSelector">Output format:</label>
|
|
</td>
|
|
<td style="vertical-align: middle;">
|
|
<select id="formatSelector">
|
|
<option value="wled" selected>WLED JSON</option>
|
|
<option value="curl">CURL</option>
|
|
<option value="ha">Home Assistant YAML</option>
|
|
</select>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="vertical-align: middle;">
|
|
<label for="colorFormatSelector">Color code format:</label>
|
|
</td>
|
|
<td style="vertical-align: middle;">
|
|
<select id="colorFormatSelector">
|
|
<option value="hex" selected>HEX (#f4f4f4)</option>
|
|
<option value="dec">DEC (244,244,244)</option>
|
|
</select>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="vertical-align: middle;">
|
|
<label for="addressingSelector">Addressing:</label>
|
|
</td>
|
|
<td style="vertical-align: middle;">
|
|
<select id="addressingSelector">
|
|
<option value="range" selected>Range (10, 17, #f4f4f4)</option>
|
|
<option value="single">Single (#f4f4f4)</option>
|
|
</select>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="vertical-align: middle;">
|
|
<label for="brightnessNumber">Brightness:</label>
|
|
</td>
|
|
<td style="vertical-align: middle; display: flex; align-items: center;">
|
|
<input type="range" id="brightnessNumber" min="1" max="255" value="127">
|
|
<span id="brightnessValue">100</span>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="vertical-align: middle;">
|
|
<label for="colorLimitNumber">Max no of colors/JSON:</label>
|
|
</td>
|
|
<td style="vertical-align: middle; display: flex; align-items: center;">
|
|
<input type="range" id="colorLimitNumber" min="1" max="512" value="256">
|
|
<span id="colorLimitValue" >256</span>
|
|
</td>
|
|
</tr>
|
|
<tr class="ha-hide">
|
|
<td style="vertical-align: middle;">
|
|
<label for="haID">HA Device ID:</label>
|
|
</td>
|
|
<td style="vertical-align: middle;">
|
|
<input type="text" id="haID" value="pixel_art_controller_001">
|
|
</td>
|
|
</tr>
|
|
<tr class="ha-hide">
|
|
<td style="vertical-align: middle;">
|
|
<label for="haUID">HA Device Unique ID:</label>
|
|
</td>
|
|
<td style="vertical-align: middle;">
|
|
<input type="text" id="haUID" value="pixel_art_controller_001a">
|
|
</td>
|
|
</tr>
|
|
<tr class="ha-hide">
|
|
<td style="vertical-align: middle;">
|
|
<label for="haName">HA Device Name:</label>
|
|
</td>
|
|
<td style="vertical-align: middle;">
|
|
<input type="text" id="haName" value="Pixel Art Kitchen">
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="vertical-align: middle;">
|
|
<label for="curlUrl">Device IP/host name:</label>
|
|
</td>
|
|
<td style="vertical-align: middle;">
|
|
<input type="text" id="curlUrl" value="">
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="vertical-align: middle;">
|
|
<label for="renderCheckbox">Show pixel rendering</label>
|
|
</td>
|
|
<td style="vertical-align: middle;">
|
|
<input type="checkbox" id="renderCheckbox" checked>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="vertical-align: middle;">
|
|
<label for="helpCheckbox">Show help/about</label>
|
|
</td>
|
|
<td style="vertical-align: middle;">
|
|
<input type="checkbox" id="helpCheckbox">
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
|
|
|
|
</p>
|
|
</p>
|
|
|
|
<div id="help-container" style="display: none">>
|
|
<p>
|
|
1a. Create a GIF or PNG ( <a href="https://www.pixilart.com/" target="_blank">www.pixilart.com</a> ) <br>
|
|
1b. Download some image that fits your led matrix ( <a href="https://www.spriters-resource.com/" target="_blank">www.spriters-resource.com</a>) <br>
|
|
2. Upload the image using the file picker or drag-and-drop it <br>
|
|
3. Select the led setup that matches your WLED. ( <a href="https://kno.wled.ge/advanced/mapping/" target="_blank">https://kno.wled.ge</a>) <br>
|
|
4. Select Otput format. <br>
|
|
- WLED is pure JSON in the way the documentation descripbes it.<br> Use in WLED (presets?)<br>
|
|
- CURL is formated as a curl command you can past into a command <br> window and test yor led matrix<br>
|
|
- Home Assistant is teh full YAML you can past into your <br> configuration.yaml in Home Assistant<br>
|
|
5. Select Color format. Select HEX if possible (more efficiant)<br>
|
|
6. Select Addressing scheme. Send all pixels individually or try to <br> send ranges of the same color.<br>
|
|
7. Select brighness value<br>
|
|
8. According to docs WLED can handle max 256 colors/command. So the JSON is <br> split if you have larger images. <br> Lower this value if you have issues.<br>
|
|
9. Set Home Assistant Device values if you are going to use HA<br>
|
|
10. Set the device IP/host for HA and CURL to work<br>
|
|
11. Press the convert button <br>
|
|
12. Copy the generated JSON and put it somewhere in WLED<br> , Run CURL or paste to Home Assistant
|
|
</p>
|
|
<p>
|
|
This tool is a proof of concept and work in progress. As you might expect, there is absolutely no warranty, what so ever.
|
|
</p>
|
|
<p>
|
|
The Arcade font is copyright (c) Jakob Fischer at www.pizzadude.dk, all rights reserved. Do not distribute without the author's permission.
|
|
</p>
|
|
<p>
|
|
It should be said that I don‘t acctually own a setup for this. I basically did it to play around over new years. But I have ordered a matrix now, and a small controller chip... let's see how that works out.
|
|
</p>
|
|
</div>
|
|
|
|
<p>
|
|
<label for="file-picker">
|
|
<div id="drop-zone">
|
|
Drop image here <br>or <br>
|
|
Click to select a file
|
|
</div>
|
|
</label>
|
|
</p>
|
|
|
|
<p>
|
|
<input type="file" id="file-picker" style="display: none;">
|
|
<div style="width: 100%; text-align: center;" >
|
|
<img id="preview" style="display: block; margin: 0 auto;"><br>
|
|
</div>
|
|
|
|
<form id="form">
|
|
<input id="submitConvert" type="submit" value="Convert to JSON for WLED" style="display: none">
|
|
</form>
|
|
|
|
<div id="raw-image-container" style="display: none">
|
|
<img id="image" src="" alt="RawImage image">
|
|
</div>
|
|
</p>
|
|
|
|
<div id="image-container" style="display: none">
|
|
<div id="image-info" style="display: none"></div>
|
|
<p>
|
|
<div>
|
|
<textarea id="JSONled"></textarea>
|
|
<br>
|
|
<button id="copyJSONledbutton" >Copy led-JSON to Clipboard</button>
|
|
<br>
|
|
<button id="sendJSONledbutton" >Send to Device</button>
|
|
<br>
|
|
</div>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id=bottom-part style="display: none" class=bottom-part></div>
|
|
<canvas id="pixelCanvas"></canvas>
|
|
</div>
|
|
<script>
|
|
var curlStart='curl -X POST "http://',curlMid1="/json/state\" -d '",curlEnd='\' -H "Content-Type: application/json"';const haStart="#Uncomment if you don't allready have these defined in your switch section of your configuration.yaml\n#- platform: command_line\n #switches:\n ",haMid1="\n friendly_name: ",haMid2="\n unique_id: ",haMid3="\n command_on: >\n ",haMid4='\n command_off: >\n curl -X POST "http://',haEnd='/json/state" -d \'{"on":false}\' -H "Content-Type: application/json"',haCommandLeading=" ",JSONledStringStart='{"on":true, "bri":',JSONledStringMid1=', "seg":{"i":[',JSONledShortStringStart="{",JSONledShortStringMid1='"seg":{"i":[',JSONledStringEnd="]}}";console.log(location.host),document.getElementById("curlUrl").value=location.host;let httpArray=[];async function postPixels(){for(let e of httpArray)try{console.log(e),console.log(e.length);let t=await fetch("http://"+document.getElementById("curlUrl").value+"/json/state",{method:"POST",headers:{"Content-Type":"application/json"},body:e}),n=await t.json();console.log(n)}catch(l){console.error(l)}}document.getElementById("form").addEventListener("submit",function(e){e.preventDefault();let t=document.getElementById("preview").src;if(isValidBase64Gif(t))document.getElementById("image").src=t,getPixelRGBValues(t),document.getElementById("image-container").style.display="block";else{let n=document.getElementById("image-info");n.innerHTML="<p><b>WARNING!</b> File does not appear to be a valid GIF image</p>",n.style.display="block",document.getElementById("image-container").style.display="none",document.getElementById("JSONled").value="",console.log("The string '"+t+"' is not a valid base64 GIF image.")}}),copyJSONledbutton.addEventListener("click",async()=>{JSONled.select();try{await navigator.clipboard.writeText("test text"),console.log("Text copied to clipboard")}catch(e){console.error("Failed to copy text: ",e)}}),sendJSONledbutton.addEventListener("click",async()=>{"https:"===window.location.protocol?alert("Will only be available when served over http (or WLED is run over https)"):postPixels()});let helpCheckbox=document.getElementById("helpCheckbox"),helpDiv=document.getElementById("help-container");helpCheckbox.addEventListener("change",function(){helpCheckbox.checked?helpDiv.style.display="block":helpDiv.style.display="none"});const dropZone=document.getElementById("drop-zone"),filePicker=document.getElementById("file-picker"),preview=document.getElementById("preview");function zoneClicked(e){e.preventDefault(),filePicker.click()}function dragEnter(e){e.preventDefault(),this.classList.add("drag-over")}function dragOver(e){e.preventDefault()}function dropped(e){e.preventDefault(),this.classList.remove("drag-over");let t=e.dataTransfer.files[0];updatePreview(t)}function filePicked(e){let t=e.target.files[0];updatePreview(t)}function updatePreview(e){let t=new FileReader;t.onload=function(){preview.src=t.result,document.getElementById("submitConvert").style.display="block"},t.readAsDataURL(e)}function isValidBase64Gif(e){return!0}dropZone.addEventListener("dragenter",dragEnter),dropZone.addEventListener("dragover",dragOver),dropZone.addEventListener("drop",dropped),dropZone.addEventListener("click",zoneClicked),filePicker.addEventListener("change",filePicked),document.getElementById("brightnessNumber").oninput=function(){document.getElementById("brightnessValue").textContent=this.value},document.getElementById("colorLimitNumber").oninput=function(){document.getElementById("colorLimitValue").textContent=this.value};for(var formatSelector=document.getElementById("formatSelector"),hideableRows=document.querySelectorAll(".ha-hide"),i=0;i<hideableRows.length;i++)hideableRows[i].classList.add("hide");function getPixelRGBValues(e){httpArray=[],document.getElementById("copyJSONledbutton");let t=document.getElementById("JSONled"),n=document.getElementById("colorLimitNumber").value,l=-1,a=document.getElementById("formatSelector");l=a.selectedIndex;let o=a.options[l].value;l=(a=document.getElementById("ledSetupSelector")).selectedIndex;let r=a.options[l].value;l=(a=document.getElementById("colorFormatSelector")).selectedIndex;let d=!0;"dec"==a.options[l].value&&(d=!1),l=(a=document.getElementById("addressingSelector")).selectedIndex;let s=!0;"single"==a.options[l].value&&(s=!1);let c="",h="",g="'",u="'";d||(g="[",u="]");let m=!1,p="";var y=document.createElement("canvas"),f=y.getContext("2d"),v=new Image;v.src=e,v.onload=function(){y.width=v.width,y.height=v.height,p="<p>Width: "+v.width+", Height: "+v.height+" (make sure this matches your led matrix setup)</p>",f.drawImage(v,0,0);var e=f.getImageData(0,0,v.width,v.height).data,l=[];let a=1;"l2r"==r&&(a=0);for(var E=0;E<e.length;E+=4){var I=e[E],B=e[E+1],b=e[E+2],w=e[E+3];let $=E/4,S=Math.floor($/v.width),_=$;if("matrix"==r);else if((S+a)%2==0);else{let x=_-S*v.width,k=v.width-1-x;_=S*v.width+k}l.push([I,B,b,w,_,$,S])}l.sort((e,t)=>e[5]-t[5]);let L=[...l];L.sort((e,t)=>e[4]-t[4]);let C="",T=-1,N=L.length,R=0,P=[];for(let O=0;O<N;O++){let D=L[O],M=D[0],j=D[1],A=D[2],H=D[3],W="",G=-1;if(s){if(T<0&&(T=O),O<N-1){let J=L[O+1];(J[0]!=M||J[1]!=j||J[2]!=A)&&(W=T+","+(G=O+1)+",")}else W=T+","+(G=O+1)+","}else""==C&&(C=O+", 'dummy',"),T=O,G=O;if(H<255&&(m=!0),G>-1){let U=M+","+j+","+A;if(d){let[V,F,Z]=[M,j,A];U=`${[V,F,Z].map(e=>e.toString(16).padStart(2,"0")).join("")}`}C=C+W+g+U+u,(R+=1)%n==0||O==N-1?(P.push(C),C=""):C+=",",T=-1}}C="";for(let z=0;z<P.length;z++){let q='{"on":true, "bri":'+document.getElementById("brightnessNumber").value+', "seg":{"i":['+P[z]+"]}}";httpArray.push(q);let X=curlStart+document.getElementById("curlUrl").value+curlMid1+q+curlEnd;z>0&&(C+="\n",c+=" && "),C+=q,c+=X}h="#Uncomment if you don't allready have these defined in your switch section of your configuration.yaml\n#- platform: command_line\n #switches:\n "+document.getElementById("haID").value+"\n friendly_name: "+document.getElementById("haName").value+"\n unique_id: "+document.getElementById("haUID").value+haMid3+c+haMid3+document.getElementById("curlUrl").value+'/json/state" -d \'{"on":false}\' -H "Content-Type: application/json"',"wled"==o?t.value=C:"curl"==o?t.value=c:"ha"==o?t.value=h:t.value="ERROR!/n"+o+" is an unknown format.";let K=document.getElementById("image-info"),Q=document.getElementById("image-info");m&&(p+="<p><b>WARNING!</b> Transparency info detected in image. Transparency (alpha) has been ignored. To ensure you get the result you desire, use only solid colors in your image.</p>"),K.innerHTML=p,Q.style.display="block",drawBoxes(l,v.width,v.width)}}function drawBoxes(e,t,n){var l=document.getElementById("pixelCanvas"),a=l.getContext("2d");window.innerHeight<window.innerWidth?l.width=Math.floor(.98*window.innerHeight):l.width=Math.floor(.98*window.innerWidth);let o=Math.floor(l.width/t);l.height=o*n+10;for(let r=0;r<n;r++)for(let d=0;d<t;d++){let s=e[r*t+d],c="rgb("+s[0]+", "+s[1]+", "+s[2]+")";s[0],s[1],s[2];let h=s[4];a.fillStyle=c,a.fillRect(d*o,r*o,o,o),a.strokeStyle="#888888",a.lineWidth=1,a.strokeRect(d*o,r*o,o,o),a.font="10px Arial",a.fillStyle="rgb(128,128,128)",a.textAlign="center",a.textBaseline="middle",a.fillText(h+1,d*o+o/2,r*o+o/2)}}function drawBackground(){let e=document.createElement("div");e.id="grid",e.classList.add("grid-class"),e.style.cssText="";let t=Math.ceil(window.innerWidth/20)*Math.ceil(window.innerHeight/20);for(let n=0;n<t;n++){let l=document.createElement("div");l.classList.add("box"),l.style.backgroundColor=getRandomColor(),e.appendChild(l)}e.style.zIndex=-1,document.body.appendChild(e)}function getRandomColor(){let e="rgba(";for(let t=0;t<3;t++)e+=Math.floor(256*Math.random())+",";return e+"0.05)"}formatSelector.addEventListener("change",function(){for(var e=0;e<hideableRows.length;e++)hideableRows[e].classList.toggle("hide","ha"!==this.value)}),window.drawBackground=drawBackground;
|
|
</script>
|
|
</body>
|
|
</html> |