Files
PixelArtConverter/pixartmin.html
Henrik e608ee41d7 Minifyed with all files merged
Intended to minimize load on the WLED device. Small file size, same functionality, absolutely impossible to modify localy (and stay sane)
2023-01-15 12:17:43 +01:00

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>&nbsp;&nbsp;&nbsp;Use in WLED (presets?)<br>
- CURL is formated as a curl command you can past into a command <br>&nbsp;&nbsp;&nbsp;window and test yor led matrix<br>
- Home Assistant is teh full YAML you can past into your <br>&nbsp;&nbsp;&nbsp;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>&nbsp;&nbsp;&nbsp;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>&nbsp;&nbsp;&nbsp;split if you have larger images. <br>&nbsp;&nbsp;&nbsp;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>&nbsp;&nbsp;&nbsp;, 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&lsquo;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>