Showing preview only (204K chars total). Download the full file or copy to clipboard to get everything.
Repository: werkstrom/WLED-PixelArtConverter
Branch: main
Commit: 755377286fbe
Files: 21
Total size: 195.5 KB
Directory structure:
gitextract_fy85wnci/
├── .github/
│ └── workflows/
│ ├── env.yml
│ └── manual.yml
├── LICENSE
├── README.md
├── RELEASENOTES.md
├── archived/
│ ├── pixart.htm
│ └── pixartmin.htm
├── beta/
│ ├── pixart.htm
│ └── releasenotes.md
├── examples/
│ └── filename
├── html/
│ ├── boxdraw.js
│ ├── getPixelValues.js
│ ├── index.html
│ ├── index.js
│ ├── pixartmin.html
│ ├── site.webmanifest
│ ├── statics.js
│ └── styles.css
├── infoFiles/
│ ├── FlowDesignDocument.md
│ └── runnerFlowTemplate.json
└── pixart.htm
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/env.yml
================================================
version_string: "Version ${{ inputs.version }}"
================================================
FILE: .github/workflows/manual.yml
================================================
# This is a basic workflow that is manually triggered
name: Manual workflow
# Controls when the action will run. Workflow runs when manually triggered using the UI
# or API.
on:
workflow_dispatch:
# Inputs the workflow accepts.
inputs:
version:
# Friendly description to be shown in the UI instead of 'name'
description: 'Version (YY.M.X)'
# Default value if no value is explicitly provided
default: '99.1.1'
# Input has to be provided for the workflow to run
required: true
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "greet"
greet:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Runs a single command using the runners shell
- name: Checkout
uses: actions/checkout@v3.3.0
with:
ref: main
- name: Pull the latest changes
run: git pull
- name: Check if the file is present
run: ls
- name: Check if the file is present
run: ls html/
- name: Copy and Rename File
run: cp html/index.html pixartmin.htm
- name: Check if the file is present
run: ls
- name: Check if the file is present
run: ls html/
- name: Add the changes
run: git add pixartmin.htm
- name: Use env file
env:
envFile: manualenv.yml
run: |
echo "The version is ${{ manualenv.version_string }}"
- name: Commit the changes
run: git -c user.name=${{ secrets.NAME }} -c user.email=${{ secrets.EMAIL }} commit -m ${{ manualenv.version_string }}
- name: Push the changes
run: git push
#run: git push git@github.com:myname/myproject.git
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2023 Henrik Werkström
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Download instructions
For stable (1.0.8)
***Right***-Click [here](https://raw.githubusercontent.com/werkstrom/WLED-PixelArtConverter/main/pixart.htm) and select save to your local computer.
For beta (1.1.0)
***Right***-Click [here](https://raw.githubusercontent.com/werkstrom/WLED-PixelArtConverter/main/beta/pixart.htm) and select save to your local computer.
Acctual text may differ depending on browser, but for Chrome (as of writing) the menu item would to select be *[Save link as...]*.
# Additional instructions
There are now instructions directly in the WLED documentation [here](https://kno.wled.ge/features/pixel-art-converter/). As the Pixel Art Converter project migrates to main WLED project, documentation will most likely primarily be done there as it is beneficial to have a complete, end-to-end documentation on how to set up WLED and use Pixel Art Converter together.
# WLED-PixelArtConverter
Convert any image your browser can render into pixel art for WLED
This is a tool for making it easy to show pixel art on a LED panel run by WLED
Find out more about the awesome [WLED software](https://kno.wled.ge/) or their [GitHub](https://github.com/Aircoookie/WLED)
* Please read "Current notes" at the end of the document for known issues and important hints.*
* *To run* ***directly*** *from WLED device use pixartmin.html according to* ***instructions below.***
* As the PixelArtConverter has moved into the main WLED project, sourcecode will no longer be available here. Only downloadable stand alones and documentation.
* With the official WLED V0.14.0-b1 there are some issues with the 2D matrix implementation. It's the first release, so no surprise. Many (all known) are fixed and available in the nightly builds. You can get one here but be aware it is development builds. [https://wled-install.github.io/](https://wled-install.github.io/)
## Most notable functions
- You can convert almost any image (tested with at least PNG, JPG, WEBP, GIF)
- You can upload the generated settings directly to your WLED device, with a click of a button
- If you want to trigger your image from Home Assistant the tool also generates the entire YAML for your configuration.yaml
- If you want to trigger your image from any other place/system the tool generates a complete CURL command
- You can adjust the format you send the data to WLED in(to fit your specific setup, i.e. slow or limited devices)
- Generates several types led orders, to fit your setup (though with 2D setup you should not need to anymore)
- Very small footprint. Only about 35 kB excluding favicons
- 100% pure Javascript/html/CSS
- Runs in your browser only, no sending iimages to any cloud services
- Communicates with your device(s) to get valid options.
## Basic Operations
1. First download the pixart.htm file and save it to some where on your computer. Any OS should do really since its purely run inside the browser. *Note: As of V 1.0.8 only the minified version is distributed as it has all the functionality, but in a smaller package.*
2. And open the pixart.htm file in any modern browser.
3. Select the settings according to your settup and what you want to do.
4. Select an image (or drag and drop one) and make sure the preview is visable (the very small image). This indicates your image can be read by the tool
5. Click the button below to convert your image.
6. You should be able to se a large preview of the generated led data at the bottom of the page
**A. Direct upload**
7. Press the button to send your image to your WLED device.
8. Validate the image reached the device and is looking as expected. If not change the parameters in PixelArtConverer and try again.
9. Create a preset on your device from the current setting (follow the instructions [here](https://kno.wled.ge/features/presets/) TL;DR: Press the "Create Preset" button.
10. If you want to upload the same image to another device, simply change the Device IP/host name and click "Send to device" again
**B. Use elsewhere**
7. Copy the generated code if you want to use it outside of the PixelArtConverter/WLED Note: Since you are running the tool localy, you will not be allowed to copy to your clipboard automatically. You will ned to mark the entire text (ctrl+a) and copy it (ctrl+c).
8. Paste the text into your tool of choice. **Note: The Preset API setup (where you can paste JSON and create a preset) does currently not support multiple commands, i.e. your entire settings JSON *must* be in one single command string.**
## Running directly on your WLED device
*As PixelArtConverter is no merged into the main WLED project. This option will shortly be available as default*
Most WLED devices are low power very limited compute so it is important to make life as easy as possible for it. That's why there is a one-file-solution for you to use if you want to make PixelArtConverter available by accessing your WLED device. Follow these steps:
1. Download the *pixart.htm* file
2. Direct you browser to http://[your.device.ip]/edit
3. Upload the *pixart.htm* file using the UI
4. Direct you browser to http://[your.device.ip]/pixart.htm
And you should be good to go.
## Notes and tips
### Image Size
You can resize your images to fit your led setup. The image is projected exactly as it is, onto exactly the size you provide. That means that if your source file aspect ratio does not fit your target size aspect ratio, your image will get destorted. Still in many cases it is not noticable.
### Output format
Pretty self explainatory. If you are to upload to your device from PixelArtConverter you should select WLED JSON
### Color code format
Though WLED can handle both. The documentation explicitly states HEX is faster and requires less resources. Use it whenever you can.
### Addressing
PixelArtConverter can either generate one color code per led/pixel, Or, it can identify ranges within your image. Many times the ranges option generates fewer commands and is thus faster and more reliable. But not allways. If you upload to your device and saves it as a preset, it doesn't matter. The preset is based on WLED's internal format and is thus optimized. But if you are to trigger the image from another place, like Home Assistant it can make a lot of difference, depending on your image. The Ukranian flag (that everybody knows by now) is MUCH faster and easier in range mode, but a chess board migt actually be slower...
As of version 1.0.1 there is a hybrid mode that utilizes the ability to mix single and range. this should be the default for anyone to use from now on.
### Number of colors per command
WLED is running on very limited devices, and you're throwing a massive amount af data on them in a very short time. To make things easy on the devices, limit your number of colors. You can test with your device how much it can handle. But this will vary.
## Target segment id
If multiple segements are set up in WLED you can send it to any one of them. The most convenient way to set a valid target is to query the device for valid IDs. Simply make sure your device's IP/host is set in the field above and click the "Get from cloud" icon. If connection is successful, the icon briefly turns green and you get a selection of available segment IDs to select from. If connection is unsuccesfull the icon turs red briefly in which case you can still manually set a segment and generate code for later use. To reset the selection change the IP/host and click the button again.
### Home Assistant
The code generated will create a switch for you within home Assistant. What you do with it, how you trigger it, how you make it look in Home Assistant... Anything you wonder about that. Head over to the [Home Assistant Forum](https://community.home-assistant.io/)
### Where to find images
If you want to create your own or modify/edit then [www.pixilart.com](https://www.pixilart.com) is an excelent tool
If you want to find old sprites from old games try out [www.spriters-resource.com](https://www.spriters-resource.com) or [opengameart.org](https://opengameart.org/)
There are also some (a few) here in the folder "examples"
## Current notes
### http, https and the issue with **mixed content** in web browsers
As a security meassure web browser do not allow you to access resources (web panges and web services) through http (un encrypted) from a page (like PixelArtConverter) when it is loaded through https (encrypted). Also public webservers (like [ledcalculator.werkstrom.com](https://ledcalculator.werkstrom.com)) "must" (in practise) use https. This means two things.
1. In order to upload directly to your device, PixelArtConverter **must** be loaded in the same security context (http/https) as WLED. Since most WLED devices are local IoT devices it's mainly http. That's why you need to download the files and run them locally. A more convenient sollution is to have WLED serve you the page directly as that will solve any issues with possible missmatch. As of today you can set this up manually by uploading the files to your device @ [your.device.ip]/edit and then loading it to your browser using [your.device.ip]/index.html. We're looking into adding it in a more convenient way.
2. You cannot use the tool @ [ledcalculator.werkstrom.com](https://ledcalculator.werkstrom.com) to upload **directly** to your device. The generated JSON, CURL and Home Assistant code will work just fine though.
### Possible issue with single addressing after WLED V0.14.0-b1
As of V0.14.0-b1 there is a bug in WLED that PixelArtConverter have a workaround for that will probably break your upload if you
1. Use **Single** addressing (and you shouldn't)
2. Have a later build of WLED.
It should in practise not be a problem, but keep in mind if you get issues
================================================
FILE: RELEASENOTES.md
================================================
# Version 1.0.6
## Main take aways
- Streamlined the conversion process
- Image generates as soon as there is enough input to generete an image
- Image regenerates on value changes. No need to press convert button
- Segments are now named as on the device
- If are selected, and rescale is active, w/h will be automatically set from the selected segment
- Buttons that communicate with the device is more informative of success (green) or fail (red)
- Convertbutton removed (since there is no use for pressing it any more)
- Minor bugfixes and adjustments
# Version 1.0.5
## Main take aways
- Querying the device for available segments
- Manually input any segment id (0-63)
- Preview matrix is now centered also on landscape displays
- Several UI adjustments
- Alignment with WLED profile
- Development moved to main WLED project
- Changed file ending to htm to align with WLED naming
# Version 1.0.4
## Main take aways
- Minor visual adjustments
# Version 1.0.3
## Main take aways
- Possible to send your image to different segments. There appear to be some sort of issue we havent identified, so currently only segment 0 works. Bur we need to get the function in there to test, so... Test on!
- Minor adjustments and bugfixes
# Version 1.0.2
## Main take aways
- Developer mode: Some new and experimental features will be turned off unless you run in developer mode. To enter developermode append "?dev" to the url. Example: http://192.168.0.123/pixartmin.htm?dev
- File to device: Experimental feature to send the generated code as a file to the device instead of using the JSON API. Will not do anything more on the device at the moment.
- Minor adjustmensts
================================================
FILE: archived/pixart.htm
================================================
<!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>
WLED Pixel Art Converter</title> <style>
.box{border:2px solid #fff}body{font-family:Arial,sans-serif;background-color:#111}.top-part{width:600px;margin:0 auto}.container{max-width:100% -40px;border-radius:0;padding:20px;text-align:center}h1{font-size:2.3em;color:#ddd;margin:1px 0;font-family:Arial,sans-serif;line-height:.5}h2{font-size:1.1em;color:rgba(221,221,221,.61);margin:1px 0;font-family:Arial,sans-serif;line-height:.5;text-align:center}h3{font-size:.7em;color:rgba(221,221,221,.61);margin:1px 0;font-family:Arial,sans-serif;line-height:1.4;text-align:center;align-items:center;justify-content:center;display:flex}p{font-size:1em;color:#777;line-height:1.5;font-family:Arial,sans-serif}#fieldTable{font-size:1 em;color:#777;line-height:1;font-family:Arial,sans-serif}#scaleTable{font-size:1 em;color:#777;line-height:1;font-family:Arial,sans-serif}#drop-zone{display:block;width:100%-40px;border:3px dashed #ddd;border-radius:0;text-align:center;padding:20px;margin:0;cursor:pointer;font-family:Arial,sans-serif;font-size:15px;color:#777}#file-picker{display:none}.adaptiveTD{display:flex;flex-direction:row;flex-wrap:nowrap;align-items:center}.mainSelector{background-color:#222;color:#ddd;border:1px solid #333;margin-top:4px;margin-bottom:4px;padding:0 8px;height:28px;font-size:15px;border-radius:7px;flex-grow:1;display:flex;align-items:center;justify-content:center}.adaptiveSelector{background-color:#222;color:#ddd;border:1px solid #333;margin-top:4px;margin-bottom:4px;padding:0 8px;height:28px;font-size:15px;border-radius:7px;flex-grow:1;display:none}.segmentsDiv{width:36px;padding-left:5px}* input[type=range]{appearance:none;-moz-appearance:none;-webkit-appearance:none;flex-grow:1;padding:0;margin:4px 8px 4px 0;background-color:transparent;cursor:pointer;background:linear-gradient(to right,#bbb 50%,#333 50%);border-radius:7px}input[type=range]:focus{outline:0}input[type=range]::-webkit-slider-runnable-track{height:28px;cursor:pointer;background:0 0;border-radius:7px}input[type=range]::-webkit-slider-thumb{height:16px;width:16px;border-radius:50%;background:#fff;cursor:pointer;-webkit-appearance:none;margin-top:4px;border-radius:7px}input[type=range]::-moz-range-track{height:28px;background-color:rgba(0,0,0,0);border-radius:7px}input[type=range]::-moz-range-thumb{border:0 solid transparent;height:16px;width:16px;border-radius:7px;background:#fff}.rangeNumber{width:20px;vertical-align:middle}.fullTextField[type=text]{background-color:#222;border:1px solid #333;padding-inline-start:5px;margin-top:4px;margin-bottom:4px;height:24px;border-radius:0;font-family:Arial,sans-serif;font-size:15px;color:#ddd;border-radius:7px;flex-grow:1;display:flex;align-items:center;justify-content:center}.flxTFld{background-color:#222;border:1px solid #333;padding-inline-start:5px;height:24px;border-radius:0;font-family:Arial,sans-serif;font-size:15px;color:#ddd;border-radius:7px;flex-grow:1;display:flex;align-items:center;justify-content:center}* input[type=submit]{background-color:#222;border:1px solid #333;padding:.5em;width:100%;border-radius:24px;font-family:Arial,sans-serif;font-size:1.3em;color:#ddd}* button{background-color:#222;border:1px solid #333;padding-inline:5px;width:100%;border-radius:24px;font-family:Arial,sans-serif;font-size:1em;color:#ddd;display:flex;align-items:center;justify-content:center;cursor:pointer}#scaleDiv{display:flex;align-items:center;vertical-align:middle}textarea{grid-row:1/2;width:100%;height:200px;background-color:#222;border:1px solid #333;color:#ddd}.hide{display:none}.svg-icon{vertical-align:middle}#image-container{display:grid;grid-template-rows:1fr 1fr}#button-container{display:flex;padding-bottom:10px;padding-top:10px}.buttonclass{flex:1;padding-top:5px;padding-bottom:5px}.gap{width:10px}#submitConvert::before{content:"";display:inline-block;background-image:url('data:image/svg+xml;utf8, <svg style="width:24px;height:24px" viewBox="0 0 24 24" <path fill="currentColor" d="M12,6V9L16,5L12,1V4A8,8 0 0,0 4,12C4,13.57 4.46,15.03 5.24,16.26L6.7,14.8C6.25,13.97 6,13 6,12A6,6 0 0,1 12,6M18.76,7.74L17.3,9.2C17.74,10.04 18,11 18,12A6,6 0 0,1 12,18V15L8,19L12,23V20A8,8 0 0,0 20,12C20,10.43 19.54,8.97 18.76,7.74Z" /></svg>');width:36px;height:36px}#sizeDiv *{display:inline-block}.sizeInputFields{width:50px;background-color:#222;border:1px solid #333;padding-inline-start:5px;margin-top:-5px;height:24px;border-radius:7px;font-family:Arial,sans-serif;font-size:15px;color:#ddd}a:link{color:rgba(221,221,221,.61);background-color:transparent;text-decoration:none}a:visited{color:rgba(221,221,221,.61);background-color:transparent;text-decoration:none}a:hover{color:#ddd;background-color:transparent;text-decoration:none}a:active{color:rgba(221,221,221,.61);background-color:transparent;text-decoration:none}
</style> <link rel="shortcut icon"
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAkNJREFUOE+lUstO21AUnHNsF0V+0EXVivAXfAe7qlL7EZEcQ5cQ8nCICbDJF7Trvhas6YbyDdBu2hSk7kihscK1fU91HRKKaFf1xhrP8ejMnCH850P/+v/cC2qcqw4gsGD3Hk/G23+bvSdw7nk1K887BeCQ4AGMAiEjcGaB7gndEfhRqdRQSC9ncQikAVisAWGBFggR5QvQ7UcTNd9mLvDdC2p2rhIN2G4U/vRbneyynzhps70oAPlhNPLjZna1v4/xVmtQHU8tzQXO3CCk4nrPD8MLN+6qd8ef8erwBKvH750XK1Xy4li9/fQFrw9PAGSDg8azuwJDN6jbuep7a+GFF3ezN0en5fDq8Qfn+UqVgjhW5bePpwCKwUHj6a2ACY7zrCdCC34UjrzutjLZiQC/kh0HlyPyu9tKzM4iGPf7uG5sDEwWpYWvrh85RZYAYAIp7+XayNtqytXeLqWbjUUNcLC+fuG2mpLu7tPl5sZDAnpLk0mzFDir+BFJlkCECaKFuSBYSlA4pLWtiRjEygIyErFzyIKjreYTlbZKgaHrRlzoHotweXeAhEhIxPBTDBhQYgEYhNbybINhxY1sbSwwkbHOZrzs3VRmjmnOk6bOkrqxMHT9yCqyxNybAFMg1mWRBGYlg2eboOSFGdT+IwM3Eil6zBwXZF1xkSWkOa6qtHleceuQvC+g4pZXiaOt9jyDb24Q2qKC5XTSvrlK3ZHMW04nLdMPKtQOMzoGTzPz69YNP2vi7D11ftvQWYCzyt7jfwO10TQgL5hT6QAAAABJRU5ErkJggg==">
<script type="text/javascript">
function gId(e){return d.getElementById(e)}function cE(e){return d.createElement(e)}var d=document
</script> </head> <body> <body> <div class="top-part"> <div
style="display:flex;justify-content:center"> <h1
style="display:flex;align-items:center"> <svg
style="width:36px;height:36px;margin-right:6px" viewBox="0 0 32 32"><path
fill="#003fff" d="M6 22h8v4H6zM14 14h4v8h-4zM18 10h4v8h-4zM22 6h8v4h-8z"/></svg>
WLED Pixel Art Converter </h1> </div> <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 class="adaptiveTD"> <select id="ledSetupSelector"
class="mainSelector"> <option value="matrix" selected="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 class="adaptiveTD">
<select id="formatSelector" class="mainSelector"> <option value="wled"
selected="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 class="adaptiveTD"> <select
id="colorFormatSelector" class="mainSelector"> <option value="hex"
selected="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 class="adaptiveTD">
<select id="addressingSelector" class="mainSelector"> <option value="hybrid"
selected="selected">Hybrid (#f0f0f0,10, 17, #f4f4f4)</option> <option
value="range">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="128"> <span
id="brightnessValue">128</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
class="adaptiveTD"> <input class="fullTextField" 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 class="adaptiveTD"> <input class="fullTextField" 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 class="adaptiveTD"> <input class="fullTextField" 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 class="adaptiveTD"> <input class="fullTextField" type="text"
id="curlUrl" value> </td> </tr> <tr> <td style="vertical-align:middle"> <label
for="targetSegment">Target segment id:</label> </td> <td class="adaptiveTD">
<input class="flxTFld" type="number" id="segID" value="0" min="0" max="63">
<select id="targetSegment" class="adaptiveSelector"> </select> <div
id="getSegmentsDiv" class="segmentsDiv"></div> </td> </tr> </table> <table
class="scaleTableClass" id="scaleTable"
style="width:100%;table-layout:fixed;align-content:center"> <tr> <td
style="vertical-align:middle"> <div id="scaleDiv"> <svg
style="width:36px;height:36px" viewBox="0 0 24 24" onclick="switchScale()"
cursor="pointer"><path fill="currentColor"
d="M17 7H7a5 5 0 0 0-5 5 5 5 0 0 0 5 5h10a5 5 0 0 0 5-5 5 5 0 0 0-5-5M7 15a3 3 0 0 1-3-3 3 3 0 0 1 3-3 3 3 0 0 1 3 3 3 3 0 0 1-3 3z"/>
</svg> Scale image </div> </td> <td style="vertical-align:middle"> <div
id="sizeDiv" style="display:none"> <label for="sizeX">W : </label> <input
class="sizeInputFields" type="number" id="sizeX" value="0">
<label for="sizeY">H : </label> <input class="sizeInputFields"
type="number" id="sizeY" value="0"> </div> </td> </tr> </table> </p> <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"> </p><div
style="width:100%;text-align:center"> <img id="preview"
style="display:none;margin:0 auto" src="data:text/html;base64,"> </div> <div
id="submitConvertDiv" style="display:none"> <button id="convertbutton"
class="buttonclass"></button> </div> <div id="raw-image-container"
style="display:none"> <img id="image" src="data:text/html;base64,"
alt="RawImage image"> </div> <p></p> <div id="image-container"
style="display:none"> <div id="image-info" style="display:none"></div> <textarea
id="JSONled"></textarea> </div> <div id="button-container"
style="display:none"> <button id="copyJSONledbutton" class="buttonclass">
</button> <div id="gap1" class="gap"></div> <button id="sendJSONledbutton"
class="buttonclass"></button> <div id="gap2" class="gap"></div> <button
id="fileJSONledbutton" class="buttonclass"></button> </div> <div> <h3><div
id="version">Version 1.0.5</div> - <a
href="https://github.com/werkstrom/WLED-PixelArtConverter/blob/main/README.md"
target="_blank">Help/About</a></h3> </div> </div> <div id="bottom-part"
style="display:none" class="bottom-part"></div> <canvas id="pixelCanvas">
</canvas> <script type="text/javascript">
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":{"id":',JSONledStringMid2=',"i":[',JSONledStringEnd="]}}";var accentColor="#eee",accentTextColor="#777",greenColor="#056b0a",redColor="#6b050c",scaleToggleOffd="M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M7,15A3,3 0 0,1 4,12A3,3 0 0,1 7,9A3,3 0 0,1 10,12A3,3 0 0,1 7,15Z",scaleToggleOnd="M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M17,15A3,3 0 0,1 14,12A3,3 0 0,1 17,9A3,3 0 0,1 20,12A3,3 0 0,1 17,15Z"
</script> <script type="text/javascript">
function getPixelRGBValues(e){httpArray=[],fileJSON=JSONledStringStart+gId("brightnessNumber").value+JSONledStringMid1+gId("targetSegment").value+JSONledStringMid2;const t=gId("targetSegment");let l=0;console.log(t.style.display),l="flex"==t.style.display?t.value:gId("segID").value,console.log(l);const a=gId("JSONled"),d=gId("colorLimitNumber").value;let r=!1,i=-1,n=gId("formatSelector");i=n.selectedIndex;const o=n.options[i].value;n=gId("ledSetupSelector"),i=n.selectedIndex;const g=n.options[i].value;n=gId("colorFormatSelector"),i=n.selectedIndex;let s=!0;"dec"==n.options[i].value&&(s=!1),n=gId("addressingSelector"),i=n.selectedIndex;let u=!0;"single"==n.options[i].value?u=!1:"hybrid"==n.options[i].value&&(r=!0);let c="",h="",S="'",I="'";s||(S="[",I="]");let v=!1,p="";var f=cE("canvas"),m=f.getContext("2d"),N=new Image;N.src=e,N.onload=function(){let e=gId("scaleDiv").children[0].children[0].getAttribute("fill"),t=gId("sizeX").value,i=gId("sizeY").value;(e!=accentColor||t<1||i<1)&&(t=N.width,i=N.height),f.width=t,f.height=i,p="<p>Width: "+t+", Height: "+i+" (make sure this matches your led matrix setup)</p>",m.drawImage(N,0,0,t,i);var n=m.getImageData(0,0,t,i).data,y=[];let O=1;"l2r"==g&&(O=0);for(var J=0;J<n.length;J+=4){var b=n[J],M=n[J+1],x=n[J+2],w=n[J+3];let e=J/4,l=Math.floor(e/t),a=e;if("matrix"==g);else if((l+O)%2==0);else{a=l*t+(t-1-(a-l*t))}y.push([b,M,x,w,a,e,l])}y.sort((e,t)=>e[5]-t[5]);let E=[...y];E.sort((e,t)=>e[4]-t[4]);let D="",R=-1,A=E.length,T=0,k=[];for(let e=0;e<A;e++){let t=E[e],l=t[0],a=t[1],i=t[2],n=t[3],o="",g=-1;if(u)if(R<0&&(R=e),e<A-1){let t=E[e+1];t[0]==l&&t[1]==a&&t[2]==i||(g=e+1,o=R==e&&r?""==D?e+",":"":R+","+g+",")}else g=e+1,o=R+1==g&&r?""==D?e+",":"":R+","+g+",";else""==D&&(D=e),R=e,g=e;if(n<255&&(v=!0),g>-1){let t=l+","+a+","+i;if(s){const[e,d,r]=[l,a,i];t=""+[e,d,r].map(e=>e.toString(16).padStart(2,"0")).join("")}D=D+o+S+t+I,fileJSON=D+o+S+t+I,T+=1,T%d==0||e==A-1?(k.push(D),D=""):D+=",",R=-1}}D="";for(let e=0;e<k.length;e++){let t=JSONledStringStart+gId("brightnessNumber").value+JSONledStringMid1+l+JSONledStringMid2+k[e]+JSONledStringEnd;httpArray.push(t);let a=curlStart+gId("curlUrl").value+curlMid1+t+curlEnd;e>0&&(D+="\n",c+=" && "),D+=t,c+=a}h=haStart+gId("haID").value+haMid1+gId("haName").value+haMid2+gId("haUID").value+haMid3+c+haMid3+gId("curlUrl").value+haEnd,a.value="wled"==o?D:"curl"==o?c:"ha"==o?h:"ERROR!/n"+o+" is an unknown format.",fileJSON+=JSONledStringEnd;let U=gId("image-info"),z=gId("image-info");v&&(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>"),U.innerHTML=p,z.style.display="block",drawBoxes(y,t,i)}}
</script> <script type="text/javascript">
function drawBoxes(t,e,i){var n=gId("pixelCanvas"),l=n.getContext("2d");window.innerHeight<window.innerWidth?n.width=Math.floor(.98*window.innerHeight):n.width=Math.floor(.98*window.innerWidth);let o=Math.floor(n.width/e),d=(window.innerWidth-e*o)/2;n.height=o*i+10;for(let n=0;n<i;n++)for(let i=0;i<e;i++){let d=t[n*e+i],h="rgb("+d[0]+", "+d[1]+", "+d[2]+")",r=(d[0],d[1],d[2],d[4]),w="rgb(128,128,128)";l.fillStyle=h,l.fillRect(i*o,n*o,o,o),l.strokeStyle="#888888",l.lineWidth=1,l.strokeRect(i*o,n*o,o,o),l.font="10px Arial",l.fillStyle=w,l.textAlign="center",l.textBaseline="middle",l.fillText(r+1,i*o+o/2,n*o+o/2)}var h=l.getImageData(0,0,n.width,n.height);l.clearRect(0,0,n.width,n.height),n.width=window.innerWidth,l.putImageData(h,d,0),console.log(d)}
</script> <script type="text/javascript">
gId("curlUrl").value=location.host;let devMode=!1;const urlParams=new URLSearchParams(window.location.search);urlParams.has("dev")&&(devMode=!0),devMode?console.log("Developer mode active. Experimental and unstable functions active."):console.log('Developer mode inactive. Append "?dev" to the URL.'),devMode?(gId("fileJSONledbutton").style.display="buttonclass",gId("gap2").style.display="gap"):(gId("fileJSONledbutton").style.display="none",gId("gap2").style.display="none");let httpArray=[],fileJSON="";async function postPixels(){for(let e of httpArray)try{devMode&&console.log(e),devMode&&console.log(e.length);const t=await fetch("http://"+gId("curlUrl").value+"/json/state",{method:"POST",headers:{"Content-Type":"application/json"},body:e}),l=await t.json();devMode&&console.log(l)}catch(e){console.error(e)}}gId("convertbutton").addEventListener("click",()=>{let e=gId("preview").src;if(isValidBase64Gif(e))gId("image").src=e,getPixelRGBValues(e),gId("image-container").style.display="block",gId("button-container").style.display="";else{let t=gId("image-info"),l="<p><b>WARNING!</b> File does not appear to be a valid image</p>";t.innerHTML=l,t.style.display="block",gId("image-container").style.display="none",gId("JSONled").value="",devMode&&console.log("The string '"+e+"' is not a valid base64 image.")}}),gId("copyJSONledbutton").addEventListener("click",async()=>{let e=gId("JSONled");e.select();try{await navigator.clipboard.writeText(e.value)}catch(e){try{await d.execCommand("copy")}catch(e){console.error("Failed to copy text: ",e)}}}),gId("sendJSONledbutton").addEventListener("click",async()=>{"https:"===window.location.protocol?alert("Will only be available when served over http (or WLED is run over https)"):postPixels()}),gId("fileJSONledbutton").addEventListener("click",async()=>{if("https:"===window.location.protocol)alert("Will only be available when served over http (or WLED is run over https)");else{let e="TheName.json",t="http://"+gId("curlUrl").value+"/upload";sendAsFile(fileJSON,e,t)}});const dropZone=gId("drop-zone"),filePicker=gId("file-picker"),preview=gId("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");updatePreview(e.dataTransfer.files[0])}function filePicked(e){updatePreview(e.target.files[0])}function updatePreview(e){const t=new FileReader;t.onload=()=>{preview.src=t.result,gId("submitConvertDiv").style.display="",gId("preview").style.display=""},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),gId("brightnessNumber").oninput=()=>{let e=gId("brightnessNumber");gId("brightnessValue").textContent=e.value;let t=100*parseInt(e.value)/255;var l=`linear-gradient(90deg, #bbb ${t}%, #333 ${t}%)`;e.style.backgroundImage=l},gId("colorLimitNumber").oninput=()=>{let e=gId("colorLimitNumber");gId("colorLimitValue").textContent=e.value;let t=100*parseInt(e.value)/512;var l=`linear-gradient(90deg, #bbb ${t}%, #333 ${t}%)`;e.style.backgroundImage=l};for(var hideableRows=d.querySelectorAll(".ha-hide"),i=0;i<hideableRows.length;i++)hideableRows[i].classList.add("hide");function switchScale(){let e=gId("scaleDiv").children[0].children[0],t=e.getAttribute("fill"),l="";t===accentColor?(t=accentTextColor,l=scaleToggleOffd,gId("sizeDiv").style.display="none"):(t=accentColor,l=scaleToggleOnd,gId("sizeDiv").style.display=""),e.setAttribute("fill",t),e.setAttribute("d",l)}function sendAsFile(e,t,l){var o=new Blob([e],{type:"application/json"});devMode&&(console.log(e),console.log(t),console.log(l));var n=new FormData;n.append("file",o,t);var i=new XMLHttpRequest;i.open("POST",l,!0),i.onload=()=>{200===i.status?devMode&&console.log("File uploaded successfully!"):devMode&&console.log("File upload failed!")},i.send(n)}function generateSegmentOptions(e){var t=gId("targetSegment");t.innerHTML="";for(var l=0;l<e.length;l++){var o=cE("option");o.value=e[l].value,o.text=e[l].text,t.appendChild(o),0===l&&(o.selected=!0)}}async function getSegments(){try{var e=[];const o=await fetch("http://"+gId("curlUrl").value+"/json/state"),n=await o.json();console.log(n);let i=n.seg.map(e=>e.id);console.log(i);for(var t=0;t<i.length;t++){e.push({value:i[t],text:"Segment index "+i[t]}),generateSegmentOptions(e),gId("targetSegment").style.display="flex",gId("segID").style.display="none";var l=gId("getSegmentsSVGpath");l.setAttribute("fill",greenColor),setTimeout((function(){l.setAttribute("fill",accentTextColor)}),1e3)}}catch(e){console.error(e);var o=gId("getSegmentsSVGpath");o.setAttribute("fill",redColor),setTimeout((function(){o.setAttribute("fill",accentTextColor)}),1e3),gId("targetSegment").style.display="none",gId("segID").style.display="flex"}}function generateSegmentArray(e){for(var t=[],l=0;l<e;l++)t.push({value:l,text:"Segment index "+l});return t}gId("formatSelector").addEventListener("change",()=>{for(var e=0;e<hideableRows.length;e++)hideableRows[e].classList.toggle("hide","ha"!==gId("formatSelector").value)});var segmentData=generateSegmentArray(10);generateSegmentOptions(segmentData),gId("getSegmentsDiv").innerHTML='<svg id=getSegmentsSVG style="width:36px;height:36px;cursor:pointer" viewBox="0 0 24 24" onclick="getSegments()"><path id=getSegmentsSVGpath fill="currentColor" d="M6.5 20Q4.22 20 2.61 18.43 1 16.85 1 14.58 1 12.63 2.17 11.1 3.35 9.57 5.25 9.15 5.68 7.35 7.38 5.73 9.07 4.1 11 4.1 11.83 4.1 12.41 4.69 13 5.28 13 6.1V12.15L14.6 10.6L16 12L12 16L8 12L9.4 10.6L11 12.15V6.1Q9.1 6.45 8.05 7.94 7 9.43 7 11H6.5Q5.05 11 4.03 12.03 3 13.05 3 14.5 3 15.95 4.03 17 5.05 18 6.5 18H18.5Q19.55 18 20.27 17.27 21 16.55 21 15.5 21 14.45 20.27 13.73 19.55 13 18.5 13H17V11Q17 9.8 16.45 8.76 15.9 7.73 15 7V4.68Q16.85 5.55 17.93 7.26 19 9 19 11 20.73 11.2 21.86 12.5 23 13.78 23 15.5 23 17.38 21.69 18.69 20.38 20 18.5 20M12 11.05Z" /></svg>',gId("fileJSONledbutton").innerHTML='<svg style="width:36px;height:36px" viewBox="0 0 24 24"><path fill="currentColor" d="M20 18H4V8H20M20 6H12L10 4H4A2 2 0 0 0 2 6V18A2 2 0 0 0 4 20H20A2 2 0 0 0 22 18V8A2 2 0 0 0 20 6M16 17H14V13H11L15 9L19 13H16Z" /></svg> File to device',gId("convertbutton").innerHTML='<svg style="width:36px;height:36px" viewBox="0 0 24 24"><path fill="currentColor" d="M12,6V9L16,5L12,1V4A8,8 0 0,0 4,12C4,13.57 4.46,15.03 5.24,16.26L6.7,14.8C6.25,13.97 6,13 6,12A6,6 0 0,1 12,6M18.76,7.74L17.3,9.2C17.74,10.04 18,11 18,12A6,6 0 0,1 12,18V15L8,19L12,23V20A8,8 0 0,0 20,12C20,10.43 19.54,8.97 18.76,7.74Z" /> </svg> Convert to WLED JSON ',gId("copyJSONledbutton").innerHTML='<svg class="svg-icon" style="width:36px;height:36px" viewBox="0 0 24 24"> <path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z" /> </svg> Copy to clipboard',gId("sendJSONledbutton").innerHTML='<svg class="svg-icon" style="width:36px;height:36px" viewBox="0 0 24 24"> <path fill="currentColor" d="M6.5 20Q4.22 20 2.61 18.43 1 16.85 1 14.58 1 12.63 2.17 11.1 3.35 9.57 5.25 9.15 5.88 6.85 7.75 5.43 9.63 4 12 4 14.93 4 16.96 6.04 19 8.07 19 11 20.73 11.2 21.86 12.5 23 13.78 23 15.5 23 17.38 21.69 18.69 20.38 20 18.5 20H13Q12.18 20 11.59 19.41 11 18.83 11 18V12.85L9.4 14.4L8 13L12 9L16 13L14.6 14.4L13 12.85V18H18.5Q19.55 18 20.27 17.27 21 16.55 21 15.5 21 14.45 20.27 13.73 19.55 13 18.5 13H17V11Q17 8.93 15.54 7.46 14.08 6 12 6 9.93 6 8.46 7.46 7 8.93 7 11H6.5Q5.05 11 4.03 12.03 3 13.05 3 14.5 3 15.95 4.03 17 5.05 18 6.5 18H9V20M12 13Z" /> </svg> Send to device'
</script> </body> </body></html>
================================================
FILE: archived/pixartmin.htm
================================================
<!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>
WLED Pixel Art Converter</title> <style>
.box{border:2px solid #fff}body{font-family:Arial,sans-serif;background-color:#111}.top-part{width:600px;margin:0 auto}.container{max-width:100% -40px;border-radius:0;padding:20px;text-align:center}h1{font-size:2.3em;color:#ddd;margin:1px 0;font-family:Arial,sans-serif;line-height:.5}h2{font-size:1.1em;color:rgba(221,221,221,.61);margin:1px 0;font-family:Arial,sans-serif;line-height:.5;text-align:center}h3{font-size:.7em;color:rgba(221,221,221,.61);margin:1px 0;font-family:Arial,sans-serif;line-height:1.4;text-align:center;align-items:center;justify-content:center;display:flex}p{font-size:1em;color:#777;line-height:1.5;font-family:Arial,sans-serif}#fieldTable{font-size:1 em;color:#777;line-height:1;font-family:Arial,sans-serif}#scaleTable{font-size:1 em;color:#777;line-height:1;font-family:Arial,sans-serif}#drop-zone{display:block;width:100%-40px;border:3px dashed #ddd;border-radius:0;text-align:center;padding:20px;margin:0;cursor:pointer;font-family:Arial,sans-serif;font-size:15px;color:#777}#file-picker{display:none}.adaptiveTD{display:flex;flex-direction:row;flex-wrap:nowrap;align-items:center}.mainSelector{background-color:#222;color:#ddd;border:1px solid #333;margin-top:4px;margin-bottom:4px;padding:0 8px;height:28px;font-size:15px;border-radius:7px;flex-grow:1;display:flex;align-items:center;justify-content:center}.adaptiveSelector{background-color:#222;color:#ddd;border:1px solid #333;margin-top:4px;margin-bottom:4px;padding:0 8px;height:28px;font-size:15px;border-radius:7px;flex-grow:1;display:none}.segmentsDiv{width:36px;padding-left:5px}* input[type=range]{appearance:none;-moz-appearance:none;-webkit-appearance:none;flex-grow:1;padding:0;margin:4px 8px 4px 0;background-color:transparent;cursor:pointer;background:linear-gradient(to right,#bbb 50%,#333 50%);border-radius:7px}input[type=range]:focus{outline:0}input[type=range]::-webkit-slider-runnable-track{height:28px;cursor:pointer;background:0 0;border-radius:7px}input[type=range]::-webkit-slider-thumb{height:16px;width:16px;border-radius:50%;background:#fff;cursor:pointer;-webkit-appearance:none;margin-top:4px;border-radius:7px}input[type=range]::-moz-range-track{height:28px;background-color:rgba(0,0,0,0);border-radius:7px}input[type=range]::-moz-range-thumb{border:0 solid transparent;height:16px;width:16px;border-radius:7px;background:#fff}.rangeNumber{width:20px;vertical-align:middle}.fullTextField[type=text]{background-color:#222;border:1px solid #333;padding-inline-start:5px;margin-top:4px;margin-bottom:4px;height:24px;border-radius:0;font-family:Arial,sans-serif;font-size:15px;color:#ddd;border-radius:7px;flex-grow:1;display:flex;align-items:center;justify-content:center}.flxTFld{background-color:#222;border:1px solid #333;padding-inline-start:5px;height:24px;border-radius:0;font-family:Arial,sans-serif;font-size:15px;color:#ddd;border-radius:7px;flex-grow:1;display:flex;align-items:center;justify-content:center}* input[type=submit]{background-color:#222;border:1px solid #333;padding:.5em;width:100%;border-radius:24px;font-family:Arial,sans-serif;font-size:1.3em;color:#ddd}* button{background-color:#222;border:1px solid #333;padding-inline:5px;width:100%;border-radius:24px;font-family:Arial,sans-serif;font-size:1em;color:#ddd;display:flex;align-items:center;justify-content:center;cursor:pointer}#scaleDiv{display:flex;align-items:center;vertical-align:middle}textarea{grid-row:1/2;width:100%;height:200px;background-color:#222;border:1px solid #333;color:#ddd}.hide{display:none}.svg-icon{vertical-align:middle}#image-container{display:grid;grid-template-rows:1fr 1fr}#button-container{display:flex;padding-bottom:10px;padding-top:10px}.buttonclass{flex:1;padding-top:5px;padding-bottom:5px}.gap{width:10px}#submitConvert::before{content:"";display:inline-block;background-image:url('data:image/svg+xml;utf8, <svg style="width:24px;height:24px" viewBox="0 0 24 24" <path fill="currentColor" d="M12,6V9L16,5L12,1V4A8,8 0 0,0 4,12C4,13.57 4.46,15.03 5.24,16.26L6.7,14.8C6.25,13.97 6,13 6,12A6,6 0 0,1 12,6M18.76,7.74L17.3,9.2C17.74,10.04 18,11 18,12A6,6 0 0,1 12,18V15L8,19L12,23V20A8,8 0 0,0 20,12C20,10.43 19.54,8.97 18.76,7.74Z" /></svg>');width:36px;height:36px}#sizeDiv *{display:inline-block}.sizeInputFields{width:50px;background-color:#222;border:1px solid #333;padding-inline-start:5px;margin-top:-5px;height:24px;border-radius:7px;font-family:Arial,sans-serif;font-size:15px;color:#ddd}a:link{color:rgba(221,221,221,.61);background-color:transparent;text-decoration:none}a:visited{color:rgba(221,221,221,.61);background-color:transparent;text-decoration:none}a:hover{color:#ddd;background-color:transparent;text-decoration:none}a:active{color:rgba(221,221,221,.61);background-color:transparent;text-decoration:none}
</style> <link rel="shortcut icon"
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAkNJREFUOE+lUstO21AUnHNsF0V+0EXVivAXfAe7qlL7EZEcQ5cQ8nCICbDJF7Trvhas6YbyDdBu2hSk7kihscK1fU91HRKKaFf1xhrP8ejMnCH850P/+v/cC2qcqw4gsGD3Hk/G23+bvSdw7nk1K887BeCQ4AGMAiEjcGaB7gndEfhRqdRQSC9ncQikAVisAWGBFggR5QvQ7UcTNd9mLvDdC2p2rhIN2G4U/vRbneyynzhps70oAPlhNPLjZna1v4/xVmtQHU8tzQXO3CCk4nrPD8MLN+6qd8ef8erwBKvH750XK1Xy4li9/fQFrw9PAGSDg8azuwJDN6jbuep7a+GFF3ezN0en5fDq8Qfn+UqVgjhW5bePpwCKwUHj6a2ACY7zrCdCC34UjrzutjLZiQC/kh0HlyPyu9tKzM4iGPf7uG5sDEwWpYWvrh85RZYAYAIp7+XayNtqytXeLqWbjUUNcLC+fuG2mpLu7tPl5sZDAnpLk0mzFDir+BFJlkCECaKFuSBYSlA4pLWtiRjEygIyErFzyIKjreYTlbZKgaHrRlzoHotweXeAhEhIxPBTDBhQYgEYhNbybINhxY1sbSwwkbHOZrzs3VRmjmnOk6bOkrqxMHT9yCqyxNybAFMg1mWRBGYlg2eboOSFGdT+IwM3Eil6zBwXZF1xkSWkOa6qtHleceuQvC+g4pZXiaOt9jyDb24Q2qKC5XTSvrlK3ZHMW04nLdMPKtQOMzoGTzPz69YNP2vi7D11ftvQWYCzyt7jfwO10TQgL5hT6QAAAABJRU5ErkJggg==">
<script type="text/javascript">
function gId(e){return d.getElementById(e)}function cE(e){return d.createElement(e)}var d=document
</script> </head> <body> <body> <div class="top-part"> <div
style="display:flex;justify-content:center"> <h1
style="display:flex;align-items:center"> <svg
style="width:36px;height:36px;margin-right:6px" viewBox="0 0 32 32"><path
fill="#003fff" d="M6 22h8v4H6zM14 14h4v8h-4zM18 10h4v8h-4zM22 6h8v4h-8z"/></svg>
WLED Pixel Art Converter </h1> </div> <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 class="adaptiveTD"> <select id="ledSetupSelector"
class="mainSelector"> <option value="matrix" selected="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 class="adaptiveTD">
<select id="formatSelector" class="mainSelector"> <option value="wled"
selected="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 class="adaptiveTD"> <select
id="colorFormatSelector" class="mainSelector"> <option value="hex"
selected="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 class="adaptiveTD">
<select id="addressingSelector" class="mainSelector"> <option value="hybrid"
selected="selected">Hybrid (#f0f0f0,10, 17, #f4f4f4)</option> <option
value="range">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="128"> <span
id="brightnessValue">128</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
class="adaptiveTD"> <input class="fullTextField" 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 class="adaptiveTD"> <input class="fullTextField" 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 class="adaptiveTD"> <input class="fullTextField" 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 class="adaptiveTD"> <input class="fullTextField" type="text"
id="curlUrl" value> </td> </tr> <tr> <td style="vertical-align:middle"> <label
for="targetSegment">Target segment id:</label> </td> <td class="adaptiveTD">
<input class="flxTFld" type="number" id="segID" value="0" min="0" max="63">
<select id="targetSegment" class="adaptiveSelector"> </select> <div
id="getSegmentsDiv" class="segmentsDiv"></div> </td> </tr> </table> <table
class="scaleTableClass" id="scaleTable"
style="width:100%;table-layout:fixed;align-content:center"> <tr> <td
style="vertical-align:middle"> <div id="scaleDiv"> <svg
style="width:36px;height:36px" viewBox="0 0 24 24" onclick="switchScale()"
cursor="pointer"><path fill="currentColor"
d="M17 7H7a5 5 0 0 0-5 5 5 5 0 0 0 5 5h10a5 5 0 0 0 5-5 5 5 0 0 0-5-5M7 15a3 3 0 0 1-3-3 3 3 0 0 1 3-3 3 3 0 0 1 3 3 3 3 0 0 1-3 3z"/>
</svg> Scale image </div> </td> <td style="vertical-align:middle"> <div
id="sizeDiv" style="display:none"> <label for="sizeX">W : </label> <input
class="sizeInputFields" type="number" id="sizeX" min="1" value="16">
<label for="sizeY">H : </label> <input
class="sizeInputFields" type="number" id="sizeY" min="1" value="16"> </div>
</td> </tr> </table> </p> <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"> </p><div
style="width:100%;text-align:center"> <img id="preview"
style="display:none;margin:0 auto" src="data:text/html;base64,"> </div> <div
id="raw-image-container" style="display:none"> <img id="image"
src="data:text/html;base64," alt="RawImage image"> </div> <p></p> <div
id="image-container" style="display:none"> <div id="image-info"
style="display:none"></div> <textarea id="JSONled" readonly="readonly">
</textarea> </div> <div id="button-container" style="display:none"> <button
id="copyJSONledbutton" class="buttonclass"></button> <div id="gap1" class="gap">
</div> <button id="sendJSONledbutton" class="buttonclass"></button> <div
id="gap2" class="gap"></div> <button id="fileJSONledbutton" class="buttonclass">
</button> </div> <div> <h3><div id="version">Version 1.0.6</div> - <a
href="https://github.com/werkstrom/WLED-PixelArtConverter/blob/main/README.md"
target="_blank">Help/About</a></h3> </div> </div> <div id="bottom-part"
style="display:none" class="bottom-part"></div> <canvas id="pixelCanvas">
</canvas> <script type="text/javascript">
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":{"id":',JSONledStringMid2=',"i":[',JSONledStringEnd="]}}";var accentColor="#eee",accentTextColor="#777",prsCol="#ccc",greenColor="#056b0a",redColor="#6b050c",scaleToggleOffd="M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M7,15A3,3 0 0,1 4,12A3,3 0 0,1 7,9A3,3 0 0,1 10,12A3,3 0 0,1 7,15Z",scaleToggleOnd="M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M17,15A3,3 0 0,1 14,12A3,3 0 0,1 17,9A3,3 0 0,1 20,12A3,3 0 0,1 17,15Z"
</script> <script type="text/javascript">
function getPixelRGBValues(e){httpArray=[],fileJSON=JSONledStringStart+gId("brightnessNumber").value+JSONledStringMid1+gId("targetSegment").value+JSONledStringMid2;const t=gId("targetSegment");let l=0;l="flex"==t.style.display?t.value:gId("segID").value;const a=gId("JSONled"),d=gId("colorLimitNumber").value;let i=!1,r=-1,n=gId("formatSelector");r=n.selectedIndex;const g=n.options[r].value;n=gId("ledSetupSelector"),r=n.selectedIndex;const o=n.options[r].value;n=gId("colorFormatSelector"),r=n.selectedIndex;let s=!0;"dec"==n.options[r].value&&(s=!1),n=gId("addressingSelector"),r=n.selectedIndex;let u=!0;"single"==n.options[r].value?u=!1:"hybrid"==n.options[r].value&&(i=!0);let h="",c="",S="'",I="'";s||(S="[",I="]");let v=!1,p="";var f=cE("canvas"),m=f.getContext("2d",{willReadFrequently:!0}),N=new Image;N.src=e,N.onload=function(){let e=gId("scaleDiv").children[0].children[0].getAttribute("fill"),t=gId("sizeX").value,r=gId("sizeY").value;(e!=accentColor||t<1||r<1)&&(t=N.width,r=N.height,(N.width>512||N.height>512)&&(t=16,r=16)),f.width=t,f.height=r,p="<p>Width: "+t+", Height: "+r+" (make sure this matches your led matrix setup)</p>",m.drawImage(N,0,0,t,r);var n=m.getImageData(0,0,t,r).data,y=[];let O=1;"l2r"==o&&(O=0);for(var J=0;J<n.length;J+=4){var b=n[J],M=n[J+1],x=n[J+2],w=n[J+3];let e=J/4,l=Math.floor(e/t),a=e;if("matrix"==o);else if((l+O)%2==0);else{a=l*t+(t-1-(a-l*t))}y.push([b,M,x,w,a,e,l])}y.sort((e,t)=>e[5]-t[5]);let E=[...y];E.sort((e,t)=>e[4]-t[4]);let R="",D=-1,A=E.length,T=0,k=[];for(let e=0;e<A;e++){let t=E[e],l=t[0],a=t[1],r=t[2],n=t[3],g="",o=-1;if(u)if(D<0&&(D=e),e<A-1){let t=E[e+1];t[0]==l&&t[1]==a&&t[2]==r||(o=e+1,g=D==e&&i?""==R?e+",":"":D+","+o+",")}else o=e+1,g=D+1==o&&i?""==R?e+",":"":D+","+o+",";else""==R&&(R=e),D=e,o=e;if(n<255&&(v=!0),o>-1){let t=l+","+a+","+r;if(s){const[e,d,i]=[l,a,r];t=""+[e,d,i].map(e=>e.toString(16).padStart(2,"0")).join("")}R=R+g+S+t+I,fileJSON=R+g+S+t+I,T+=1,T%d==0||e==A-1?(k.push(R),R=""):R+=",",D=-1}}R="";for(let e=0;e<k.length;e++){let t=JSONledStringStart+gId("brightnessNumber").value+JSONledStringMid1+l+JSONledStringMid2+k[e]+JSONledStringEnd;httpArray.push(t);let a=curlStart+gId("curlUrl").value+curlMid1+t+curlEnd;e>0&&(R+="\n",h+=" && "),R+=t,h+=a}c=haStart+gId("haID").value+haMid1+gId("haName").value+haMid2+gId("haUID").value+haMid3+h+haMid3+gId("curlUrl").value+haEnd,a.value="wled"==g?R:"curl"==g?h:"ha"==g?c:"ERROR!/n"+g+" is an unknown format.",fileJSON+=JSONledStringEnd;let U=gId("image-info"),z=gId("image-info");v&&(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>"),U.innerHTML=p,z.style.display="block",drawBoxes(y,t,r)}}
</script> <script type="text/javascript">
function drawBoxes(t,e,i){var l=gId("pixelCanvas"),n=l.getContext("2d",{willReadFrequently:!0});window.innerHeight<window.innerWidth?l.width=Math.floor(.98*window.innerHeight):l.width=Math.floor(.98*window.innerWidth);let d=Math.floor(l.width/e),h=(window.innerWidth-e*d)/2;l.height=d*i+10;for(let l=0;l<i;l++)for(let i=0;i<e;i++){let h=t[l*e+i],r="rgb("+h[0]+", "+h[1]+", "+h[2]+")",o=(h[0],h[1],h[2],h[4]),w="rgb(128,128,128)";n.fillStyle=r,n.fillRect(i*d,l*d,d,d),n.strokeStyle="#888888",n.lineWidth=1,n.strokeRect(i*d,l*d,d,d),n.font="10px Arial",n.fillStyle=w,n.textAlign="center",n.textBaseline="middle",n.fillText(o+1,i*d+d/2,l*d+d/2)}var r=n.getImageData(0,0,l.width,l.height);n.clearRect(0,0,l.width,l.height),l.width=window.innerWidth,n.putImageData(r,h,0)}
</script> <script type="text/javascript">
gId("curlUrl").value=location.host;let devMode=!1;const urlParams=new URLSearchParams(window.location.search);urlParams.has("dev")&&(devMode=!0),devMode?console.log("Developer mode active. Experimental and unstable functions active."):console.log('Developer mode inactive. Append "?dev" to the URL.'),devMode?(gId("fileJSONledbutton").style.display="buttonclass",gId("gap2").style.display="gap"):(gId("fileJSONledbutton").style.display="none",gId("gap2").style.display="none");let httpArray=[],fileJSON="";function gen(){if((gId("sizeX").value>0&&gId("sizeY").value>0||"none"==gId("sizeDiv").style.display)&&gId("curlUrl").value.length>0&&"none"!=gId("preview").style.display){let e=gId("preview").src;if(isValidBase64Gif(e))gId("image").src=e,getPixelRGBValues(e),gId("image-container").style.display="block",gId("button-container").style.display="";else{let t=gId("image-info"),n="<p><b>WARNING!</b> File does not appear to be a valid image</p>";t.innerHTML=n,t.style.display="block",gId("image-container").style.display="none",gId("JSONled").value="",devMode&&console.log("The string '"+e+"' is not a valid base64 image.")}}var e=gId("getSegmentsSVGpath");if(gId("curlUrl").value.length>0)e.setAttribute("fill",accentColor);else{e.setAttribute("fill",accentTextColor);let t=gId("targetSegment");t.style.display="none",t.innerHTML="",gId("segID").style.display="flex"}}async function postPixels(){(svg=gId("sendSvgP")).setAttribute("fill",prsCol);let e=!1;for(let t of httpArray)try{devMode&&console.log(t),devMode&&console.log(t.length);const e=await fetch("http://"+gId("curlUrl").value+"/json/state",{method:"POST",headers:{"Content-Type":"application/json"},body:t}),n=await e.json();devMode&&console.log(n)}catch(t){console.error(t),e=!0}e?(svg.setAttribute("fill",redColor),setTimeout((function(){svg.setAttribute("fill",accentTextColor)}),1e3)):(svg.setAttribute("fill",greenColor),setTimeout((function(){svg.setAttribute("fill",accentColor)}),1e3))}function sendAsFile(e,t,n){(svg=gId("fileSvgP")).setAttribute("fill",prsCol);var l=new Blob([e],{type:"application/json"});devMode&&(console.log(e),console.log(t),console.log(n));var i=new FormData;i.append("file",l,t);var o=new XMLHttpRequest;o.open("POST",n,!0),o.onload=()=>{200===o.status?(svg.setAttribute("fill",greenColor),setTimeout((function(){svg.setAttribute("fill",accentTextColor)}),1e3),devMode&&console.log("File uploaded successfully!")):(svg.setAttribute("fill",redColor),setTimeout((function(){svg.setAttribute("fill",accentColor)}),1e3),devMode&&console.log("File upload failed!"))},o.send(i)}gId("copyJSONledbutton").addEventListener("click",async()=>{let e=gId("JSONled");e.select();try{await navigator.clipboard.writeText(e.value)}catch(e){try{await d.execCommand("copy")}catch(e){console.error("Failed to copy text: ",e)}}}),gId("ledSetupSelector").addEventListener("change",()=>{gen()}),gId("sizeY").addEventListener("change",()=>{gen()}),gId("sizeX").addEventListener("change",()=>{gen()}),gId("formatSelector").addEventListener("change",()=>{gen()}),gId("colorFormatSelector").addEventListener("change",()=>{gen()}),gId("addressingSelector").addEventListener("change",()=>{gen()}),gId("brightnessNumber").addEventListener("change",()=>{gen()}),gId("colorLimitNumber").addEventListener("change",()=>{gen()}),gId("haID").addEventListener("change",()=>{gen()}),gId("haUID").addEventListener("change",()=>{gen()}),gId("haName").addEventListener("change",()=>{gen()}),gId("curlUrl").addEventListener("change",()=>{gen()}),gId("targetSegment").addEventListener("change",(function(){sop=this.options[this.selectedIndex],gId("sizeX").value=sop.dataset.x,gId("sizeY").value=sop.dataset.y,gen()})),gId("segID").addEventListener("change",()=>{gen()}),gId("preview").addEventListener("load",(function(){gen()})),gId("sendJSONledbutton").addEventListener("click",async()=>{"https:"===window.location.protocol?alert("Will only be available when served over http (or WLED is run over https)"):postPixels()}),gId("fileJSONledbutton").addEventListener("click",async()=>{if("https:"===window.location.protocol)alert("Will only be available when served over http (or WLED is run over https)");else{let e="TheName.json",t="http://"+gId("curlUrl").value+"/upload";sendAsFile(fileJSON,e,t)}});const dropZone=gId("drop-zone"),filePicker=gId("file-picker"),preview=gId("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");updatePreview(e.dataTransfer.files[0])}function filePicked(e){updatePreview(e.target.files[0])}function updatePreview(e){const t=new FileReader;t.onload=()=>{preview.src=t.result,gId("preview").style.display=""},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),gId("brightnessNumber").oninput=()=>{let e=gId("brightnessNumber");gId("brightnessValue").textContent=e.value;let t=100*parseInt(e.value)/255;var n=`linear-gradient(90deg, #bbb ${t}%, #333 ${t}%)`;e.style.backgroundImage=n},gId("colorLimitNumber").oninput=()=>{let e=gId("colorLimitNumber");gId("colorLimitValue").textContent=e.value;let t=100*parseInt(e.value)/512;var n=`linear-gradient(90deg, #bbb ${t}%, #333 ${t}%)`;e.style.backgroundImage=n};for(var hideableRows=d.querySelectorAll(".ha-hide"),i=0;i<hideableRows.length;i++)hideableRows[i].classList.add("hide");function switchScale(){let e=gId("scaleDiv").children[0].children[0],t=e.getAttribute("fill"),n="";t===accentColor?(t=accentTextColor,n=scaleToggleOffd,gId("sizeDiv").style.display="none"):(t=accentColor,n=scaleToggleOnd,gId("sizeDiv").style.display=""),e.setAttribute("fill",t),e.setAttribute("d",n),gen()}function generateSegmentOptions(e){var t=gId("targetSegment");t.innerHTML="";for(var n=0;n<e.length;n++){var l=cE("option");l.value=e[n].value,l.text=e[n].text,l.dataset.x=e[n].x,l.dataset.y=e[n].y,t.appendChild(l),0===n&&(l.selected=!0)}}async function getSegments(){cv=gId("curlUrl").value;var e=gId("getSegmentsSVGpath");if(cv.length>0)try{var t=[];const l=await fetch("http://"+cv+"/json/state");let i=(await l.json()).seg.map(e=>({id:e.id,n:e.n,xs:e.start,xe:e.stop,ys:e.startY,ye:e.stopY}));for(var n=0;n<i.length;n++)t.push({value:i[n].id,text:i[n].n+" (index: "+i[n].id+")",x:i[n].xe-i[n].xs,y:i[n].ye-i[n].ys});generateSegmentOptions(t),gId("targetSegment").style.display="flex",gId("segID").style.display="none",e.setAttribute("fill",greenColor),setTimeout((function(){e.setAttribute("fill",accentColor)}),1e3)}catch(t){console.error(t),e.setAttribute("fill",redColor),setTimeout((function(){e.setAttribute("fill",accentColor)}),1e3),gId("targetSegment").style.display="none",gId("segID").style.display="flex"}else e.setAttribute("fill",redColor),setTimeout((function(){e.setAttribute("fill",accentTextColor)}),1e3),gId("targetSegment").style.display="none",gId("segID").style.display="flex"}function generateSegmentArray(e){for(var t=[],n=0;n<e;n++)t.push({value:n,text:"Segment index "+n});return t}gId("formatSelector").addEventListener("change",()=>{for(var e=0;e<hideableRows.length;e++)hideableRows[e].classList.toggle("hide","ha"!==gId("formatSelector").value)});var segmentData=generateSegmentArray(10);generateSegmentOptions(segmentData),gId("getSegmentsDiv").innerHTML='<svg id=getSegmentsSVG style="width:36px;height:36px;cursor:pointer" viewBox="0 0 24 24" onclick="getSegments()"><path id=getSegmentsSVGpath fill="currentColor" d="M6.5 20Q4.22 20 2.61 18.43 1 16.85 1 14.58 1 12.63 2.17 11.1 3.35 9.57 5.25 9.15 5.68 7.35 7.38 5.73 9.07 4.1 11 4.1 11.83 4.1 12.41 4.69 13 5.28 13 6.1V12.15L14.6 10.6L16 12L12 16L8 12L9.4 10.6L11 12.15V6.1Q9.1 6.45 8.05 7.94 7 9.43 7 11H6.5Q5.05 11 4.03 12.03 3 13.05 3 14.5 3 15.95 4.03 17 5.05 18 6.5 18H18.5Q19.55 18 20.27 17.27 21 16.55 21 15.5 21 14.45 20.27 13.73 19.55 13 18.5 13H17V11Q17 9.8 16.45 8.76 15.9 7.73 15 7V4.68Q16.85 5.55 17.93 7.26 19 9 19 11 20.73 11.2 21.86 12.5 23 13.78 23 15.5 23 17.38 21.69 18.69 20.38 20 18.5 20M12 11.05Z" /></svg>',gId("fileJSONledbutton").innerHTML='<svg style="width:36px;height:36px" viewBox="0 0 24 24"><path id=fileSvgP fill="currentColor" d="M20 18H4V8H20M20 6H12L10 4H4A2 2 0 0 0 2 6V18A2 2 0 0 0 4 20H20A2 2 0 0 0 22 18V8A2 2 0 0 0 20 6M16 17H14V13H11L15 9L19 13H16Z" /></svg> File to device',gId("copyJSONledbutton").innerHTML='<svg class="svg-icon" style="width:36px;height:36px" viewBox="0 0 24 24"> <path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z" /> </svg> Copy to clipboard',gId("sendJSONledbutton").innerHTML='<svg class="svg-icon" style="width:36px;height:36px" viewBox="0 0 24 24"> <path id=sendSvgP fill="currentColor" d="M6.5 20Q4.22 20 2.61 18.43 1 16.85 1 14.58 1 12.63 2.17 11.1 3.35 9.57 5.25 9.15 5.88 6.85 7.75 5.43 9.63 4 12 4 14.93 4 16.96 6.04 19 8.07 19 11 20.73 11.2 21.86 12.5 23 13.78 23 15.5 23 17.38 21.69 18.69 20.38 20 18.5 20H13Q12.18 20 11.59 19.41 11 18.83 11 18V12.85L9.4 14.4L8 13L12 9L16 13L14.6 14.4L13 12.85V18H18.5Q19.55 18 20.27 17.27 21 16.55 21 15.5 21 14.45 20.27 13.73 19.55 13 18.5 13H17V11Q17 8.93 15.54 7.46 14.08 6 12 6 9.93 6 8.46 7.46 7 8.93 7 11H6.5Q5.05 11 4.03 12.03 3 13.05 3 14.5 3 15.95 4.03 17 5.05 18 6.5 18H9V20M12 13Z" /> </svg> Send to device';var svg=gId("getSegmentsSVGpath");gId("curlUrl").value.length>0&&svg.setAttribute("fill",accentColor)
</script> </body> </body></html>
================================================
FILE: beta/pixart.htm
================================================
<!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>
WLED Pixel Art Converter</title> <style>
.box{border:2px solid #fff}body{font-family:Arial,sans-serif;background-color:#111}.top-part{width:600px;margin:0 auto}.container{max-width:100% -40px;border-radius:0;padding:20px;text-align:center}h1{font-size:2.3em;color:#ddd;margin:1px 0;font-family:Arial,sans-serif;line-height:.5}h2{font-size:1.1em;color:rgba(221,221,221,.61);margin:1px 0;font-family:Arial,sans-serif;line-height:.5;text-align:center}h3{font-size:.7em;color:rgba(221,221,221,.61);margin:1px 0;font-family:Arial,sans-serif;line-height:1.4;text-align:center;align-items:center;justify-content:center;display:flex}p{font-size:1em;color:#777;line-height:1.5;font-family:Arial,sans-serif}#fieldTable{font-size:1 em;color:#777;line-height:1;font-family:Arial,sans-serif}.scaleTableClass{font-size:1 em;color:#777;line-height:1;font-family:Arial,sans-serif}#drop-zone{display:block;width:100%-40px;border:3px dashed #ddd;border-radius:0;text-align:center;padding:20px;margin:0;cursor:pointer;font-family:Arial,sans-serif;font-size:15px;color:#777}#file-picker{display:none}.adaptiveTD{display:flex;flex-direction:row;flex-wrap:nowrap;align-items:center;vertical-align:middle}.mainSelector{background-color:#222;color:#ddd;border:1px solid #333;margin-top:4px;margin-bottom:4px;padding:0 8px;height:28px;font-size:15px;border-radius:7px;flex-grow:1;display:flex;align-items:center;justify-content:center}.adaptiveSelector{background-color:#222;color:#ddd;border:1px solid #333;margin-top:4px;margin-bottom:4px;padding:0 8px;height:28px;font-size:15px;border-radius:7px;flex-grow:1;display:none}.segmentsDiv{width:36px;padding-left:5px}.sndRnDiv{width:36px;padding-left:5px}* input[type=range]{appearance:none;-moz-appearance:none;-webkit-appearance:none;flex-grow:1;padding:0;margin:4px 8px 4px 0;background-color:transparent;cursor:pointer;background:linear-gradient(to right,#bbb 50%,#333 50%);border-radius:7px}input[type=range]:focus{outline:0}input[type=range]::-webkit-slider-runnable-track{height:28px;cursor:pointer;background:0 0;border-radius:7px}input[type=range]::-webkit-slider-thumb{height:16px;width:16px;border-radius:50%;background:#fff;cursor:pointer;-webkit-appearance:none;margin-top:4px;border-radius:7px}input[type=range]::-moz-range-track{height:28px;background-color:rgba(0,0,0,0);border-radius:7px}input[type=range]::-moz-range-thumb{border:0 solid transparent;height:16px;width:16px;border-radius:7px;background:#fff}.rangeNumber{width:20px;vertical-align:middle}.fullTextField[type=text]{background-color:#222;border:1px solid #333;padding-inline-start:5px;margin-top:4px;margin-bottom:4px;height:24px;border-radius:0;font-family:Arial,sans-serif;font-size:15px;color:#ddd;border-radius:7px;flex-grow:1;display:flex;align-items:center;justify-content:center}.flxTFld{background-color:#222;border:1px solid #333;padding-inline-start:5px;height:24px;border-radius:0;font-family:Arial,sans-serif;font-size:15px;color:#ddd;border-radius:7px;flex-grow:1;display:flex;align-items:center;justify-content:center;width:100%;box-sizing:border-box}.durFld{background-color:#222;border:1px solid #333;padding-inline-start:5px;height:24px;border-radius:0;font-family:Arial,sans-serif;font-size:15px;color:#ddd;border-radius:7px;flex-grow:1;display:flex;align-items:center;justify-content:center}* input[type=submit]{background-color:#222;border:1px solid #333;padding:.5em;width:100%;border-radius:24px;font-family:Arial,sans-serif;font-size:1.3em;color:#ddd}* button{background-color:#222;border:1px solid #333;padding-inline:5px;width:100%;border-radius:24px;font-family:Arial,sans-serif;font-size:1em;color:#ddd;display:flex;align-items:center;justify-content:center;cursor:pointer}#scaleDiv{display:flex;align-items:center;vertical-align:middle}#runDiv{display:flex;align-items:center;vertical-align:middle}textarea{grid-row:1/2;width:100%;height:200px;background-color:#222;border:1px solid #333;color:#ddd}.hide{display:none}.svg-icon{vertical-align:middle}#image-container{display:grid;grid-template-rows:1fr 1fr}#button-container{display:flex;padding-bottom:10px;padding-top:10px}.buttonclass{flex:1;padding-top:5px;padding-bottom:5px}.gap{width:10px}#submitConvert::before{content:"";display:inline-block;background-image:url('data:image/svg+xml;utf8, <svg style="width:24px;height:24px" viewBox="0 0 24 24" <path fill="currentColor" d="M12,6V9L16,5L12,1V4A8,8 0 0,0 4,12C4,13.57 4.46,15.03 5.24,16.26L6.7,14.8C6.25,13.97 6,13 6,12A6,6 0 0,1 12,6M18.76,7.74L17.3,9.2C17.74,10.04 18,11 18,12A6,6 0 0,1 12,18V15L8,19L12,23V20A8,8 0 0,0 20,12C20,10.43 19.54,8.97 18.76,7.74Z" /></svg>');width:36px;height:36px}#sizeDiv *{display:inline-block}.sizeInputFields{width:50px;background-color:#222;border:1px solid #333;padding-inline-start:5px;margin-top:-5px;height:24px;border-radius:7px;font-family:Arial,sans-serif;font-size:15px;color:#ddd}a:link{color:rgba(221,221,221,.61);background-color:transparent;text-decoration:none}a:visited{color:rgba(221,221,221,.61);background-color:transparent;text-decoration:none}a:hover{color:#ddd;background-color:transparent;text-decoration:none}a:active{color:rgba(221,221,221,.61);background-color:transparent;text-decoration:none}.stpIconTD{width:36px;margin:0}.stpImgTD{width:36px;margin:0}.stpDur{display:flex;align-items:center;justify-content:center}
</style> <link rel="shortcut icon"
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAkNJREFUOE+lUstO21AUnHNsF0V+0EXVivAXfAe7qlL7EZEcQ5cQ8nCICbDJF7Trvhas6YbyDdBu2hSk7kihscK1fU91HRKKaFf1xhrP8ejMnCH850P/+v/cC2qcqw4gsGD3Hk/G23+bvSdw7nk1K887BeCQ4AGMAiEjcGaB7gndEfhRqdRQSC9ncQikAVisAWGBFggR5QvQ7UcTNd9mLvDdC2p2rhIN2G4U/vRbneyynzhps70oAPlhNPLjZna1v4/xVmtQHU8tzQXO3CCk4nrPD8MLN+6qd8ef8erwBKvH750XK1Xy4li9/fQFrw9PAGSDg8azuwJDN6jbuep7a+GFF3ezN0en5fDq8Qfn+UqVgjhW5bePpwCKwUHj6a2ACY7zrCdCC34UjrzutjLZiQC/kh0HlyPyu9tKzM4iGPf7uG5sDEwWpYWvrh85RZYAYAIp7+XayNtqytXeLqWbjUUNcLC+fuG2mpLu7tPl5sZDAnpLk0mzFDir+BFJlkCECaKFuSBYSlA4pLWtiRjEygIyErFzyIKjreYTlbZKgaHrRlzoHotweXeAhEhIxPBTDBhQYgEYhNbybINhxY1sbSwwkbHOZrzs3VRmjmnOk6bOkrqxMHT9yCqyxNybAFMg1mWRBGYlg2eboOSFGdT+IwM3Eil6zBwXZF1xkSWkOa6qtHleceuQvC+g4pZXiaOt9jyDb24Q2qKC5XTSvrlK3ZHMW04nLdMPKtQOMzoGTzPz69YNP2vi7D11ftvQWYCzyt7jfwO10TQgL5hT6QAAAABJRU5ErkJggg==">
<script type="text/javascript">
function gId(e){return d.getElementById(e)}function cE(e){return d.createElement(e)}var d=document
</script> </head> <body> <body> <div class="top-part"> <div
style="display:flex;justify-content:center"> <h1
style="display:flex;align-items:center"> <svg
style="width:36px;height:36px;margin-right:6px" viewBox="0 0 32 32"><path
fill="#003fff" d="M6 22h8v4H6zM14 14h4v8h-4zM18 10h4v8h-4zM22 6h8v4h-8z"/></svg>
WLED Pixel Art Converter </h1> </div> <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 class="adaptiveTD"> <select id="ledSetupSelector"
class="mainSelector"> <option value="matrix" selected="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 class="adaptiveTD">
<select id="formatSelector" class="mainSelector"> <option value="wled"
selected="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 class="adaptiveTD"> <select
id="colorFormatSelector" class="mainSelector"> <option value="hex"
selected="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 class="adaptiveTD"> <select id="addressingSelector"
class="mainSelector"> <option value="hybrid" selected="selected">
Hybrid ("f0f0f0",10, 17, "f4f4f4")</option> <option
value="range">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="128"> <span
id="brightnessValue">128</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
class="adaptiveTD"> <input class="fullTextField" 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 class="adaptiveTD"> <input class="fullTextField" 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 class="adaptiveTD"> <input class="fullTextField" 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 class="adaptiveTD"> <input class="fullTextField" type="text"
id="curlUrl" value> </td> </tr> <tr> <td style="vertical-align:middle"> <label
for="targetSegment">Target segment id:</label> </td> <td class="adaptiveTD">
<input class="flxTFld" type="number" id="segID" value="0" min="0" max="63">
<select id="targetSegment" class="adaptiveSelector"> </select> <div
id="getSegmentsDiv" class="segmentsDiv"></div> </td> </tr> </table> <table
class="scaleTableClass" id="scaleTable"
style="width:100%;table-layout:fixed;align-content:center"> <tr> <td
style="vertical-align:middle"> <div id="scaleDiv"> <svg
style="width:36px;height:36px" viewBox="0 0 24 24" onclick="switchScale()"
cursor="pointer"><path fill="currentColor"
d="M17 7H7a5 5 0 0 0-5 5 5 5 0 0 0 5 5h10a5 5 0 0 0 5-5 5 5 0 0 0-5-5M7 15a3 3 0 0 1-3-3 3 3 0 0 1 3-3 3 3 0 0 1 3 3 3 3 0 0 1-3 3z"/>
</svg> Scale image </div> </td> <td style="vertical-align:middle"> <div
id="sizeDiv" style="display:none"> <label for="sizeX">W : </label> <input
class="sizeInputFields" type="number" id="sizeX" min="1" value="16">
<label for="sizeY">H : </label> <input
class="sizeInputFields" type="number" id="sizeY" min="1" value="16"> </div>
</td> </tr> </table> <table class="scaleTableClass" id="runTable"
style="width:100%;table-layout:fixed;align-content:center"> <tr
style="height:40px"> <td style="vertical-align:middle"> <div id="runDiv"> <svg
style="width:36px;height:36px" viewBox="0 0 24 24" onclick="switchRun()"
cursor="pointer"><path fill="currentColor"
d="M17 7H7a5 5 0 0 0-5 5 5 5 0 0 0 5 5h10a5 5 0 0 0 5-5 5 5 0 0 0-5-5M7 15a3 3 0 0 1-3-3 3 3 0 0 1 3-3 3 3 0 0 1 3 3 3 3 0 0 1-3 3z"/>
</svg> Generate flow file </div> </td> <td id="rnTD" class="adaptiveTD">
<input class="flxTFld" type="text" id="rnID" value="ID"> <div id="repRnDiv"
class="sndRnDiv" title="Toggle repeat on/off" onclick="setRep()"> </div> <div
id="prwRnDiv" class="sndRnDiv" title="Preview animation"> </div> <div
id="sndRnDiv" class="sndRnDiv" title="Send animation file to device"> </div>
</td> </tr> <tr id="rnLstTR" style="display:none"> <td
style="vertical-align:middle"> <table id="rnLst"
style="width:100%;table-layout:fixed;align-content:center;vertical-align:middle">
<tr id="stp1" data-json style="display:flex;align-items:center"> </tr> </table>
</td> </tr> </table> </p> <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"> </p><div id="lib"
style="width:100%;text-align:center"> <img id="i0"
style="display:none;margin:0 auto" alt="Library image 1" width="64" height="64"
src="data:text/html;base64,"> </div> <div id="raw-image-container"
style="display:none"> <img id="image" src="data:text/html;base64,"
alt="RawImage image"> </div> <p></p> <div id="image-container"
style="display:none"> <div id="image-info" style="display:none"></div> <textarea
id="JSONled" readonly="readonly"></textarea> </div> <div id="button-container"
style="display:none"> <button id="copyJSONledbutton" class="buttonclass">
</button> <div id="gap1" class="gap"></div> <button id="sendJSONledbutton"
class="buttonclass"></button> </div> <div> <h3><div id="version"
title="PAC version 2023.04.20.19.18.22">Version 1.1.0.b</div> - <a
href="https://github.com/werkstrom/WLED-PixelArtConverter/blob/main/README.md"
target="_blank">Help/About</a></h3> </div> </div> <div id="bottom-part"
style="display:none" class="bottom-part"></div> <canvas id="pixelCanvas">
</canvas> <p> </p><div style="width:100%;text-align:center"> <div> <h3><div
id="orig">Original image</div></h3> </div> <img id="preview"
style="display:none;margin:0 auto" src="data:text/html;base64,"> </div> <p></p>
<script type="text/javascript">
for(var gurl=gId("curlUrl"),szX=gId("sizeX"),szY=gId("sizeY"),szDiv=gId("sizeDiv"),prw=gId("preview"),lib=gId("lib"),sID=gId("segID"),JLD=gId("JSONled"),tSg=gId("targetSegment"),brgh=gId("brightnessNumber"),seDiv=gId("getSegmentsDiv"),cjb=gId("copyJSONledbutton"),frm=gId("formatSelector"),cLN=gId("colorLimitNumber"),haIDe=gId("haID"),haUe=gId("haUID"),haNe=gId("haName"),aS=gId("addressingSelector"),cFS=gId("colorFormatSelector"),lSS=gId("ledSetupSelector"),imin=gId("image-info"),imcn=gId("image-container"),bcn=gId("button-container"),im=gId("image"),scDiv=gId("scaleDiv"),rnDiv=gId("runDiv"),w=window,canvas=gId("pixelCanvas"),brgV=gId("brightnessValue"),cLV=gId("colorLimitValue"),sndRnDivgId=gId("sndRnDiv"),httpArray=[],rawRGBArray=[],fileJSON="",hideableRows=d.querySelectorAll(".ha-hide"),i=0;i<hideableRows.length;i++)hideableRows[i].classList.add("hide");var accentColor="#eeeeee",inactiveColor="#444444",accentTextColor="#777",prsCol="#ccc",greenColor="#056b0a",redColor="#6b050c",scaleToggleOffd="M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M7,15A3,3 0 0,1 4,12A3,3 0 0,1 7,9A3,3 0 0,1 10,12A3,3 0 0,1 7,15Z",scaleToggleOnd="M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M17,15A3,3 0 0,1 14,12A3,3 0 0,1 17,9A3,3 0 0,1 20,12A3,3 0 0,1 17,15Z",repSVG='<svg id="repSVG" style="width:36px;height:36px;cursor:pointer" viewBox="0 0 24 24"><path id="repPath" fill="#44444" d="M17,17H7V14L3,18L7,22V19H19V13H17M7,7H17V10L21,6L17,2V5H5V11H7V7Z"/></svg>',playSVG='<svg id=playSVG style="width:36px;height:36px;cursor:pointer" viewBox="0 0 24 24"><path fill="#eeeeee" d="M8,5.14V19.14L19,12.14L8,5.14Z"/></svg>',stopSVGPath='<path id="stopSVGPath" fill="#eee" d="M18,18H6V6H18V18Z"/>',playSVGPath='<path id="playSVGPath" fill="#eee" d="M8,5.14V19.14L19,12.14L8,5.14Z"/>',sendSVG='<svg class="svg-icon" style="width:36px;height:36px;cursor:pointer" viewBox="0 0 24 24"> <path id=sendSvgP fill="#eee" d="M6.5 20Q4.22 20 2.61 18.43 1 16.85 1 14.58 1 12.63 2.17 11.1 3.35 9.57 5.25 9.15 5.88 6.85 7.75 5.43 9.63 4 12 4 14.93 4 16.96 6.04 19 8.07 19 11 20.73 11.2 21.86 12.5 23 13.78 23 15.5 23 17.38 21.69 18.69 20.38 20 18.5 20H13Q12.18 20 11.59 19.41 11 18.83 11 18V12.85L9.4 14.4L8 13L12 9L16 13L14.6 14.4L13 12.85V18H18.5Q19.55 18 20.27 17.27 21 16.55 21 15.5 21 14.45 20.27 13.73 19.55 13 18.5 13H17V11Q17 8.93 15.54 7.46 14.08 6 12 6 9.93 6 8.46 7.46 7 8.93 7 11H6.5Q5.05 11 4.03 12.03 3 13.05 3 14.5 3 15.95 4.03 17 5.05 18 6.5 18H9V20M12 13Z" /> </svg>',sSg=gId("getSegmentsSVGpath"),sRnSVG=gId("sendRunnerSVGpath"),stpTR='<td id="stpXxXLoad" class="stpIconTD" title="Set selected image to step"><svg id=loadImgSVGXxX style="width:36px;height:36px;cursor:pointer" viewBox="0 0 24 24"><path fill="#eee" d="M15 3h4V0l5 5-5 5V7h-4V3m6 8.94V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7.06c-.06.33-.06.67-.06 1a8 8 0 0 0 8 8c.33 0 .67 0 1-.06M19 18l-4.5-6-3.5 4.5-2.5-3L5 18h14Z"/></svg></td><td id="stpXxXImgTD" class="stpImgTD"><svg id=empImgSVGXxX style="width:36px;height:36px;cursor:pointer" viewBox="0 0 24 24"><path fill="#eee" d="M15,5H17V3H15M15,21H17V19H15M11,5H13V3H11M19,5H21V3H19M19,9H21V7H19M19,21H21V19H19M19,13H21V11H19M19,17H21V15H19M3,5H5V3H3M3,9H5V7H3M3,13H5V11H3M3,17H5V15H3M3,21H5V19H3M11,21H13V19H11M7,21H9V19H7M7,5H9V3H7V5Z"/></svg></td><td id="stpXxXDur" title="Duration in ms" class="stpDur" style="vertical-align: middle;">Duration: <input class="durFld" type="number" id="stpXxXDurFld" value="50" min="0"> 1/100s</td><td id="stpXxXAdd" title="Add new step below" class="stpIconTD"><svg id=addRowSVGXxX style="width:36px;height:36px;cursor:pointer" viewBox="0 0 24 24"><path fill="#eee" d="M2,16H10V14H2M18,14V10H16V14H12V16H16V20H18V16H22V14M14,6H2V8H14M14,10H2V12H14V10Z"/></svg></td><td id="stpXxXDel" title="Delete step" class="stpIconTD"><svg id=delRowSVGXxX style="width:36px;height:36px;cursor:pointer" viewBox="0 0 24 24"><path fill="#eee" d="M2,6V8H14V6H2M2,10V12H11V10H2M14.17,10.76L12.76,12.17L15.59,15L12.76,17.83L14.17,19.24L17,16.41L19.83,19.24L21.24,17.83L18.41,15L21.24,12.17L19.83,10.76L17,13.59L14.17,10.76M2,14V16H11V14H2Z"/></svg></td>'
</script> <script type="text/javascript">
function getPixelRGBValues(e){httpArray=[],rawRGBArray=[],console.log("Generating RGB"),fileJSON=`{"on":true,"bri":${brgh.value},"seg":{"id":${tSg.value},"i":[`;let t=0;t="flex"==tSg.style.display?tSg.value:sID.value;const l=parseInt(cLN.value);let n=!1,a=-1;a=frm.selectedIndex;const i=frm.options[a].value;a=lSS.selectedIndex;const o=lSS.options[a].value;a=cFS.selectedIndex;let r=!0;"dec"==cFS.options[a].value&&(r=!1),a=aS.selectedIndex;let s=!0;"single"==aS.options[a].value?s=!1:"hybrid"==aS.options[a].value&&(n=!0);let u="",d="",c='"',h='"';r||(c="[",h="]");let p=!1,g="";var f=cE("canvas"),m=f.getContext("2d",{willReadFrequently:!0}),v=new Image;v.src=e,v.onload=function(){let e=scDiv.children[0].children[0].getAttribute("fill"),a=szX.value,y=szY.value;(e!=accentColor||a<1||y<1)&&(a=v.width,y=v.height,(v.width>512||v.height>512)&&(a=16,y=16)),f.width=a,f.height=y;new Uint8Array(a*y);g="<p>Width: "+a+", Height: "+y+" (make sure this matches your led matrix setup)</p>",m.drawImage(v,0,0,a,y);var S=m.getImageData(0,0,a,y).data;rawRGBArray=[...S.filter((e,t)=>(t+1)%4!=0)];var w=[];let I=1;"l2r"==o&&(I=0);for(var $=0;$<S.length;$+=4){var b=S[$],x=S[$+1],A=S[$+2],N=S[$+3];let e=$/4,t=Math.floor(e/a),l=e;if("matrix"==o);else if((t+I)%2==0);else{l=t*a+(a-1-(l-t*a))}w.push([b,x,A,N,l,e,t])}w.sort((e,t)=>e[5]-t[5]);let R=[...w];R.sort((e,t)=>e[4]-t[4]);let T="",O=-1,D=R.length,G=0,j=[];for(let e=0;e<D;e++){let t=R[e],a=t[0],i=t[1],o=t[2],u=t[3],d="",g=-1;if(s)if(O<0&&(O=e),e<D-1){let t=R[e+1];t[0]==a&&t[1]==i&&t[2]==o||(g=e+1,d=O==e&&n?""==T?e+",":"":O+","+g+",")}else g=e+1,d=O+1==g&&n?""==T?e+",":"":O+","+g+",";else""==T&&(T=e+","),O=e,g=e;if(u<255&&(p=!0),g>-1){let t=a+","+i+","+o;if(r){const[e,l,n]=[a,i,o];t=""+[e,l,n].map(e=>e.toString(16).padStart(2,"0")).join("")}T+=d+c+t+h,fileJSON=T+d+c+t+h,G+=1,G%l==0||e==D-1?(j.push(T),T=""):T+=",",O=-1}}T="";for(let e=0;e<j.length;e++){let l=`{"on":true,"bri":${brgh.value},"seg":{"id":${t},"i":[${j[e]}]}}`;httpArray.push(l);let n=`curl -X POST "http://${gurl.value}/json/state" -d '${l}' -H "Content-Type: application/json"`;e>0&&(T+="\n<NEXT COMMAND (multiple commands not supported in API/preset setup)>\n",u+=" && "),T+=l,u+=n}d=`#Uncomment if you don't allready have these defined in your switch section of your configuration.yaml \n#- platform: command_line \n #switches: \n ${haIDe.value} \n friendly_name: ${haNe.value} \n unique_id: ${haUe.value} \n command_on: > \n ${u} \n command_off: > \n curl -X POST "http://${gurl.value}/json/state" -d '{"on":false}' -H "Content-Type: application/json"`,JLD.value="wled"==i?T:"curl"==i?u:"ha"==i?d:"ERROR!/n"+i+" is an unknown format.",fileJSON+="]}}";let B=imin,C=imin;p&&(g+="<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>"),B.innerHTML=g,C.style.display="block",drawBoxes(w,a,y)}}
</script> <script type="text/javascript">
function drawBoxes(t,e,a){var i=window,n=canvas.getContext("2d",{willReadFrequently:!0});i.innerHeight<i.innerWidth?canvas.width=Math.floor(.98*i.innerHeight):canvas.width=Math.floor(.98*i.innerWidth);let l=Math.floor(canvas.width/e),h=(i.innerWidth-e*l)/2;canvas.height=l*a+10;for(let i=0;i<a;i++)for(let a=0;a<e;a++){let h=t[i*e+a],r="rgb("+h[0]+", "+h[1]+", "+h[2]+")",d="rgb(128,128,128)";n.fillStyle=r,n.fillRect(a*l,i*l,l,l),n.strokeStyle="#888888",n.lineWidth=1,n.strokeRect(a*l,i*l,l,l),n.font="10px Arial",n.fillStyle=d,n.textAlign="center",n.textBaseline="middle",n.fillText(h[4]+1,a*l+l/2,i*l+l/2)}var r=n.getImageData(0,0,canvas.width,canvas.height);n.clearRect(0,0,canvas.width,canvas.height),canvas.width=i.innerWidth,n.putImageData(r,h,0)}
</script> <script type="text/javascript">
gurl.value=location.host;var animTOId,aAr=[],curStp=0,isRep=0;const urlParams=new URLSearchParams(window.location.search);function gen(e=!1){if((szX.value>0&&szY.value>0||"none"==szDiv.style.display)&&gurl.value.length>0&&"none"!=prw.style.display){let t=prw.src;if(isValidBase64Gif(t)){if(im.src=t,e)return getPixelRGBValues(t,!0);getPixelRGBValues(t),imcn.style.display="block",bcn.style.display=""}else{let e="<p><b>WARNING!</b> File does not appear to be a valid image</p>";imin.innerHTML=e,imin.style.display="block",imcn.style.display="none",JLD.value=""}}if(gurl.value.length>0)gId("sSg").setAttribute("fill",accentColor);else{gId("sSg").setAttribute("fill",accentTextColor);let e=tSg;e.style.display="none",e.innerHTML="",sID.style.display="flex"}}async function postPixels(){let e=gId("sendSvgP");e.setAttribute("fill",prsCol);let t=!1;for(let e of httpArray)try{const t=await fetch("http://"+gId("curlUrl").value+"/json/state",{method:"POST",headers:{"Content-Type":"application/json"},body:e});await t.json()}catch(e){console.error(e),t=!0}t?(e.setAttribute("fill",redColor),setTimeout((function(){e.setAttribute("fill",accentTextColor)}),1e3)):(e.setAttribute("fill",greenColor),setTimeout((function(){e.setAttribute("fill",accentColor)}),1e3))}async function postAnim(e){let t=gId("sendSvgP");t.setAttribute("fill",prsCol);let n=!1,i=[`{"pixart":{"anim": "${e}"}}`];console.log(i);for(let e of i)try{const t=await fetch("http://"+gId("curlUrl").value+"/json/state",{method:"POST",headers:{"Content-Type":"application/json"},body:e});await t.json()}catch(e){console.error(e),n=!0}n?(t.setAttribute("fill",redColor),setTimeout((function(){t.setAttribute("fill",accentTextColor)}),1e3)):(t.setAttribute("fill",greenColor),setTimeout((function(){t.setAttribute("fill",accentColor)}),1e3))}gurl.value.length<1&&(gurl.value="Missing_Host"),cjb.addEventListener("click",async()=>{let e=JLD;e.select();try{await navigator.clipboard.writeText(e.value)}catch(e){try{await d.execCommand("copy")}catch(e){console.error("Failed to copy text: ",e)}}}),lSS.addEventListener("change",gen),szY.addEventListener("change",gen),szX.addEventListener("change",gen),cFS.addEventListener("change",gen),aS.addEventListener("change",gen),brgh.addEventListener("change",gen),cLN.addEventListener("change",gen),haIDe.addEventListener("change",gen),haUe.addEventListener("change",gen),haNe.addEventListener("change",gen),gurl.addEventListener("change",gen),sID.addEventListener("change",gen),prw.addEventListener("load",gen),tSg.addEventListener("change",()=>{sop=tSg.options[tSg.selectedIndex],szX.value=sop.dataset.x,szY.value=sop.dataset.y,gen()}),gId("sendJSONledbutton").addEventListener("click",async()=>{"https:"===window.location.protocol?alert("Will only be available when served over http (or WLED is run over https)"):postPixels()}),brgh.oninput=()=>{brgV.textContent=brgh.value;let e=100*parseInt(brgh.value)/255;var t=`linear-gradient(90deg, #bbb ${e}%, #333 ${e}%)`;brgh.style.backgroundImage=t},cLN.oninput=()=>{let e=cLN;cLV.textContent=e.value;let t=100*parseInt(e.value)/512;var n=`linear-gradient(90deg, #bbb ${t}%, #333 ${t}%)`;e.style.backgroundImage=n},frm.addEventListener("change",()=>{for(var e=0;e<hideableRows.length;e++)hideableRows[e].classList.toggle("hide","ha"!==frm.value),gen()});const dropZone=gId("drop-zone"),filePicker=gId("file-picker"),preview=prw;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");updatePreview(e.dataTransfer.files[0])}function filePicked(e){updatePreview(e.target.files[0])}function updatePreview(e){let t="";const n=new FileReader;n.onload=()=>{preview.src=n.result;const e=document.querySelectorAll("#lib img");let i=0;e.forEach(e=>{const t=parseInt(e.id.slice(1));t>i&&(i=t)});const s=gId("i"+i);nNo=i+1;let r="i"+nNo;nNo;s.insertAdjacentHTML("afterend",`<img id="${r}" src="${preview.src}" alt="Library image ${nNo}" title="Image ${nNo}" width="64" height="64">`),t=gId(r),t.setAttribute("data-issel",1),t.style.border="2px solid #eee",t.addEventListener("click",clkImg),toDel=gId("i0"),toDel&&toDel.parentNode.removeChild(toDel),prw.style.display="",lib.style.display=""},n.readAsDataURL(e),setFoc(t)}function setFoc(e){document.querySelectorAll("#lib img").forEach(t=>{e.srcElement!=t?(t.style.border="2px solid #444",t.setAttribute("data-isSel",0)):(t.style.border="2px solid #eee",t.setAttribute("data-isSel",1))})}function clkImg(e){setFoc(e),prw.src=e.srcElement.src}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);for(var hideableRows=d.querySelectorAll(".ha-hide"),i=0;i<hideableRows.length;i++)hideableRows[i].classList.add("hide");function switchScale(){let e=scDiv.children[0].children[0],t=e.getAttribute("fill"),n="";t===accentColor?(t=accentTextColor,n=scaleToggleOffd,szDiv.style.display="none"):(t=accentColor,n=scaleToggleOnd,szDiv.style.display=""),e.setAttribute("fill",t),e.setAttribute("d",n),gen()}function switchRun(){let e=rnDiv.children[0].children[0],t=e.getAttribute("fill"),n="";t===accentColor?(t=accentTextColor,n=scaleToggleOffd,gId("rnTD").style.display="none",gId("rnLstTR").style.display="none"):(t=accentColor,n=scaleToggleOnd,gId("rnTD").style.display="",gId("rnLstTR").style.display=""),e.setAttribute("fill",t),e.setAttribute("d",n),gen()}function setStpImg(e){i=e.srcElement.ownerSVGElement,selim="",nr=i?i.id.replace("loadImgSVG",""):e.srcElement.id.replace("loadImgSVG","");const t=gId("lib").getElementsByTagName("img");for(let e=0;e<t.length;e++){if(1==t[e].getAttribute("data-issel")){selim=t[e];break}}td=gId(`stp${nr}ImgTD`),td.innerHTML=`<img id="stp${nr}Img" src="${selim.src}" alt="Step image ${nr}" title="Image: ${selim.title}" width="36" height="36" data-baseimg=${selim.id}>`}function addStpImg(e){i=e.srcElement.ownerSVGElement,selim="",nr=i?i.id.replace("addRowSVG",""):e.srcElement.id.replace("addRowSVG","");let t=parseInt(nr)+1;thisTR=gId("stp"+nr);const n=stpTR.replace(/XxX/g,e=>t);var s=Array.from(gId("rnLst").getElementsByTagName("tr")),r=[];s.forEach((function(e){thisNr=parseInt(e.id.replace("stp","")),thisNr>nr&&r.push(thisNr)})),r.sort((function(e,t){return t-e})),r.forEach((function(e){moveStep(e,e+1)}));var l=document.createElement("tr");l.id="stp"+t,l.setAttribute("data-json",""),l.style="display: flex; align-items: center;",l.innerHTML=n,thisTR.parentNode.insertBefore(l,thisTR.nextSibling),gId("loadImgSVG"+t).addEventListener("click",setStpImg),gId("addRowSVG"+t).addEventListener("click",addStpImg),gId("delRowSVG"+t).addEventListener("click",delStpImg)}function delStpImg(e){i=e.srcElement.ownerSVGElement,selim="",nr=i?i.id.replace("delRowSVG",""):e.srcElement.id.replace("delRowSVG",""),gId("stp"+nr).remove();var t=Array.from(gId("rnLst").getElementsByTagName("tr")),n=[];t.forEach((function(e){thisNr=parseInt(e.id.replace("stp","")),thisNr>nr&&n.push(thisNr)})),n.sort(),n.forEach((function(e){moveStep(e,e-1)}))}function chkDelOK(){gId(`stp${newN}Del`).style.display=""}function moveStep(e,t){gId("stp"+e).id="stp"+t,gId(`stp${e}Load`).id=`stp${t}Load`,gId("loadImgSVG"+e).id="loadImgSVG"+t,gId(`stp${e}ImgTD`).id=`stp${t}ImgTD`;gId("empImgSVG"+e)?gId("empImgSVG"+e).id="empImgSVG"+t:gId(`stp${e}Img`).id=`stp${t}Img`,gId(`stp${e}Dur`).id=`stp${t}Dur`,gId(`stp${e}DurFld`).id=`stp${t}DurFld`,gId(`stp${e}Add`).id=`stp${t}Add`,gId("addRowSVG"+e).id="addRowSVG"+t,gId(`stp${e}Del`).id=`stp${t}Del`,gId("delRowSVG"+e).id="delRowSVG"+t}function generateSegmentOptions(e){tSg.innerHTML="";for(var t=0;t<e.length;t++){var n=cE("option");n.value=e[t].value,n.text=e[t].text,n.dataset.x=e[t].x,n.dataset.y=e[t].y,tSg.appendChild(n),0===t&&(n.selected=!0,szX.value=n.dataset.x,szY.value=n.dataset.y)}}async function getSegments(){if(cv=gurl.value,cv.length>0)try{var e=[];const n=await fetch("http://"+cv+"/json/state");let i=(await n.json()).seg.map(e=>({id:e.id,n:e.n,xs:e.start,xe:e.stop,ys:e.startY,ye:e.stopY}));for(var t=0;t<i.length;t++)e.push({value:i[t].id,text:i[t].n+" (index: "+i[t].id+")",x:i[t].xe-i[t].xs,y:i[t].ye-i[t].ys});generateSegmentOptions(e),tSg.style.display="flex",sID.style.display="none",gId("sSg").setAttribute("fill",greenColor),setTimeout((function(){gId("sSg").setAttribute("fill",accentColor)}),1e3)}catch(e){console.error(e),gId("sSg").setAttribute("fill",redColor),setTimeout((function(){gId("sSg").setAttribute("fill",accentColor)}),1e3),tSg.style.display="none",sID.style.display="flex"}else gId("sSg").setAttribute("fill",redColor),setTimeout((function(){gId("sSg").setAttribute("fill",accentTextColor)}),1e3),tSg.style.display="none",sID.style.display="flex"}function generateSegmentArray(e){for(var t=[],n=0;n<e;n++)t.push({value:n,text:"Segment index "+n});return t}function anim(){gId("sendSvgP").setAttribute("fill",inactiveColor),gId("sndRnDiv").removeEventListener("click",sendAnim),gId("sndRnDiv").title="Sending disabled while animating",gId("playSVG").innerHTML=stopSVGPath,gId("prwRnDiv").removeEventListener("click",anim),gId("prwRnDiv").addEventListener("click",stopAnim),aAr=[],curStp=0,Array.from(gId("rnLst").getElementsByTagName("tr")).forEach((function(e){const t=e.id.replace("stp",""),n=e.getElementsByClassName("stpImgTD")[0].querySelector("img").getAttribute("data-baseimg"),i=10*e.getElementsByClassName("stpDur")[0].querySelector("input").value;aAr.push({stpId:t,imgId:n,dur:i})})),animateSteps(curStp=0)}function animateSteps(e){if(e>=aAr.length||e<0)stopAnim();else{var t=aAr[e];gId(t.imgId).click(),e+1>=aAr.length&&1==isRep?nStp=0:nStp=e+1,animTOId=setTimeout((function(){animateSteps(nStp)}),t.dur)}}function stopAnim(){clearTimeout(animTOId),gId("playSVG").innerHTML=playSVGPath,gId("prwRnDiv").removeEventListener("click",stopAnim),gId("prwRnDiv").addEventListener("click",anim),gId("sendSvgP").setAttribute("fill",accentColor),gId("sndRnDiv").removeEventListener("click",sendAnim),gId("sndRnDiv").addEventListener("click",sendAnim),gId("sndRnDiv").title="Send animation file to device"}function setRep(e){pth=gId("repPath"),console.log(e,pth.getAttribute("fill"),accentColor,inactiveColor),pth.getAttribute("fill")==accentColor?(pth.setAttribute("fill",inactiveColor),isRep=0):(pth.setAttribute("fill",accentColor),isRep=1)}async function sendAnim(e){gId("sendSvgP").setAttribute("fill",inactiveColor);let t=gId("rnID").value,n={description:t,created:(new Date).toLocaleString().substring(0,16).replace(",","")};n.repeat=1===isRep,sAr=[],Array.from(gId("rnLst").getElementsByTagName("tr")).forEach((function(e){const t=e.id.replace("stp",""),n=e.getElementsByClassName("stpImgTD")[0].querySelector("img").getAttribute("data-baseimg"),i=e.getElementsByClassName("stpDur")[0].querySelector("input").value;sAr.push({stpId:t,imgId:n,dur:i})}));let i=[...new Set(sAr.map(e=>e.imgId))];sets=[];const s=await itcAr(i);n.sets=s.map(e=>e[2]),sAr.sort((e,t)=>e.stpId-t.stpId);const r=sAr.map((e,t)=>({step:t+1,commandId:e.imgId,duration:e.dur}));n.steps=r;uploadAnimation(s,r,t)}async function itcAr(e){const t=[];for(const n of e){await setImg(n);const e=httpArray.map((e,t)=>({position:t+1,command:JSON.parse(e)})),i=new Uint8Array(rawRGBArray);let s={id:n,commands:e};t.push([n,i,s])}return t}async function setImg(e){return new Promise(t=>{gId(e).click(),setTimeout(()=>{t(e)},200)})}function uploadAnimation(e,t,n){t.sort((e,t)=>e.step-t.step);var i=e.map((function(e){return[e[0],e[1]]}));let s=[],r=[],l=[],a=[];if(t.forEach((function(e,t){a.push([t,parseInt(e.duration)]);const n=i.find(t=>t[0]===e.commandId)[1];let o=[];if(0==t){for(let e=0;e<n.length;e+=3)o.push([t,e/3,n[e],n[e+1],n[e+2]]);l=o.slice(),s=o.slice()}else{let e=[];for(let i=0;i<n.length;i+=3){e.push([t,i/3,n[i],n[i+1],n[i+2]]);let r=s[i/3];r[2]==n[i]&&r[3]==n[i+1]&&r[4]==n[i+2]||o.push([t,i/3,n[i],n[i+1],n[i+2]])}s=e.slice()}r.push(...o)})),1==isRep){let e=[];l.forEach((function(t,n){let i=s[n];i[2]==t[2]&&i[3]==t[3]&&i[4]==t[4]||e.push([255,n,t[2],t[3],t[4]])})),r.push(...e)}let o=[];r.forEach((function(e,t){const n=e[1],i=n>>8&255,s=255&n;o.push(e[0],i,s,e[2],e[3],e[4])}));let d=[];a.forEach((function(e,t){const n=e[1],i=n>>8&255,s=255&n;d.push(e[0],i,s)}));const c=new XMLHttpRequest,g=new Blob([new Uint8Array(o)],{type:"application/octet-stream"});fileName=`/${n}.frm`,c.fileName=fileName,console.log(`Writing ${fileName} to device`),addListenerToHTTP(c,fileName),c.open("POST","/upload");var p=new FormData;p.append("data",g,fileName),c.send(p);const u=new XMLHttpRequest,m=new Blob([new Uint8Array(d)],{type:"application/octet-stream"});fileName=`/${n}.ani`,u.fileName=fileName,console.log(`Writing ${fileName} to device`),addListenerToHTTP(u,fileName),u.open("POST","/upload");var f=new FormData;return f.append("data",m,fileName),u.send(f),!1}function addListenerToHTTP(e,t){e.addEventListener("load",(function(){console.log(`Writing ${t} succeeded. `,this.responseText," - ",this.status),gId("sendSvgP").setAttribute("fill",greenColor),setTimeout((function(){gId("sendSvgP").setAttribute("fill",accentColor)}),1e3)})),e.addEventListener("error",(function(e){console.log("Error: ",e),console.log(" Status: ",this.status),gId("sendSvgP").setAttribute("fill",greenColor),setTimeout((function(){gId("sendSvgP").setAttribute("fill",accentColor)}),2e3)}))}frm.addEventListener("change",()=>{for(var e=0;e<hideableRows.length;e++)hideableRows[e].classList.toggle("hide","ha"!==frm.value)}),document.addEventListener("dblclick",(function(){stopAnim()}));var segmentData=generateSegmentArray(10);generateSegmentOptions(segmentData),seDiv.innerHTML='<svg id=getSegmentsSVG style="width:36px;height:36px;cursor:pointer" viewBox="0 0 24 24" onclick="getSegments()"><path id=sSg fill="currentColor" d="M6.5 20Q4.22 20 2.61 18.43 1 16.85 1 14.58 1 12.63 2.17 11.1 3.35 9.57 5.25 9.15 5.68 7.35 7.38 5.73 9.07 4.1 11 4.1 11.83 4.1 12.41 4.69 13 5.28 13 6.1V12.15L14.6 10.6L16 12L12 16L8 12L9.4 10.6L11 12.15V6.1Q9.1 6.45 8.05 7.94 7 9.43 7 11H6.5Q5.05 11 4.03 12.03 3 13.05 3 14.5 3 15.95 4.03 17 5.05 18 6.5 18H18.5Q19.55 18 20.27 17.27 21 16.55 21 15.5 21 14.45 20.27 13.73 19.55 13 18.5 13H17V11Q17 9.8 16.45 8.76 15.9 7.73 15 7V4.68Q16.85 5.55 17.93 7.26 19 9 19 11 20.73 11.2 21.86 12.5 23 13.78 23 15.5 23 17.38 21.69 18.69 20.38 20 18.5 20M12 11.05Z" /></svg>',cjb.innerHTML='<svg class="svg-icon" style="width:36px;height:36px" viewBox="0 0 24 24"> <path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z" /> </svg> Copy to clipboard',gId("sendJSONledbutton").innerHTML='<svg class="svg-icon" style="width:36px;height:36px" viewBox="0 0 24 24"> <path id=sendSvgP fill="currentColor" d="M6.5 20Q4.22 20 2.61 18.43 1 16.85 1 14.58 1 12.63 2.17 11.1 3.35 9.57 5.25 9.15 5.88 6.85 7.75 5.43 9.63 4 12 4 14.93 4 16.96 6.04 19 8.07 19 11 20.73 11.2 21.86 12.5 23 13.78 23 15.5 23 17.38 21.69 18.69 20.38 20 18.5 20H13Q12.18 20 11.59 19.41 11 18.83 11 18V12.85L9.4 14.4L8 13L12 9L16 13L14.6 14.4L13 12.85V18H18.5Q19.55 18 20.27 17.27 21 16.55 21 15.5 21 14.45 20.27 13.73 19.55 13 18.5 13H17V11Q17 8.93 15.54 7.46 14.08 6 12 6 9.93 6 8.46 7.46 7 8.93 7 11H6.5Q5.05 11 4.03 12.03 3 13.05 3 14.5 3 15.95 4.03 17 5.05 18 6.5 18H9V20M12 13Z" /> </svg> Send to device',gId("repRnDiv").innerHTML=repSVG,gId("prwRnDiv").innerHTML=playSVG,gId("sndRnDiv").innerHTML=sendSVG,gId("repPath").setAttribute("fill",accentColor);const regex=/XxX/g;gId("stp1").innerHTML=stpTR.replace(regex,e=>1),gId("rnTD").style.display="none",gId("loadImgSVG1").addEventListener("click",setStpImg),gId("addRowSVG1").addEventListener("click",addStpImg),gId("delRowSVG1").addEventListener("click",delStpImg),gId("sndRnDiv").addEventListener("click",sendAnim),gId("prwRnDiv").addEventListener("click",anim),gurl.value.length>0&&gId("sSg").setAttribute("fill",accentColor)
</script> </body> </body></html>
================================================
FILE: beta/releasenotes.md
================================================
Beta
================================================
FILE: examples/filename
================================================
================================================
FILE: html/boxdraw.js
================================================
function drawBoxes(inputPixelArray, widthPixels, heightPixels) {
// Get a reference to the canvas element
var canvas = document.getElementById('pixelCanvas');
// Get the canvas context
var ctx = canvas.getContext('2d');
// Set the width and height of the canvas
if (window.innerHeight < window.innerWidth) {
canvas.width = Math.floor(window.innerHeight * 0.98);
}
else{
canvas.width = Math.floor(window.innerWidth * 0.98);
}
//canvas.height = window.innerWidth;
let pixelSize = Math.floor(canvas.width/widthPixels);
//Set the canvas height to fit the right number of pixelrows
canvas.height = (pixelSize * heightPixels) + 10
//Iterate through the matrix
for (let y = 0; y < heightPixels; y++) {
for (let x = 0; x < widthPixels; x++) {
// Calculate the index of the current pixel
let i = (y*widthPixels) + x;
//Gets the RGB of the current pixel
let pixel = inputPixelArray[i];
let pixelColor = 'rgb(' + pixel[0] + ', ' + pixel[1] + ', ' + pixel[2] + ')';
let r = pixel[0];
let g = pixel[1];
let b = pixel[2];
let pos = pixel[4];
let textColor = 'rgb(128,128,128)';
// Set the fill style to the pixel color
ctx.fillStyle = pixelColor;
//Draw the rectangle
ctx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
// Draw a border on the box
ctx.strokeStyle = '#888888';
ctx.lineWidth = 1;
ctx.strokeRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
//Write text to box
ctx.font = "10px Arial";
ctx.fillStyle = textColor;
ctx.textAlign = "center";
ctx.textBaseline = 'middle';
ctx.fillText((pos + 1), (x * pixelSize) + (pixelSize /2), (y * pixelSize) + (pixelSize /2));
}
}
}
function drawBackground() {
const grid = document.createElement("div");
grid.id = "grid";
grid.classList.add("grid-class");
grid.style.cssText = "";
const boxSize = 20;
const boxCount = Math.ceil(window.innerWidth / boxSize) * Math.ceil(window.innerHeight / boxSize);;
for (let i = 0; i < boxCount; i++) {
const box = document.createElement("div");
box.classList.add("box");
box.style.backgroundColor = getRandomColor();
grid.appendChild(box);
}
grid.style.zIndex = -1;
document.body.appendChild(grid);
}
function getRandomColor() {
const letters = "0123456789ABCDEF";
let color = "rgba(";
for (let i = 0; i < 3; i++) {
color += Math.floor(Math.random() * 256) + ",";
}
color += "0.05)";
return color;
}
window.drawBackground = drawBackground;
================================================
FILE: html/getPixelValues.js
================================================
function getPixelRGBValues(base64Image) {
httpArray = [];
fileJSON = JSONledStringStart + document.getElementById('brightnessNumber').value + JSONledStringMid1 + document.getElementById('targetSegment').value + JSONledStringMid2;
//const copyJSONledbutton = document.getElementById('copyJSONledbutton');
const JSONled = document.getElementById('JSONled');
const maxNoOfColorsInCommandSting = document.getElementById('colorLimitNumber').value;
let hybridAddressing = false;
let selectedIndex = -1;
let selector = document.getElementById("formatSelector");
selectedIndex = selector.selectedIndex;
const formatSelection = selector.options[selectedIndex].value;
selector = document.getElementById("ledSetupSelector");
selectedIndex = selector.selectedIndex;
const ledSetupSelection = selector.options[selectedIndex].value;
selector = document.getElementById("colorFormatSelector");
selectedIndex = selector.selectedIndex;
let hexValueCheck = true;
if (selector.options[selectedIndex].value == 'dec'){
hexValueCheck = false
}
selector = document.getElementById("addressingSelector");
selectedIndex = selector.selectedIndex;
let segmentValueCheck = true; //If Range or Hybrid
if (selector.options[selectedIndex].value == 'single'){
segmentValueCheck = false
} else if (selector.options[selectedIndex].value == 'hybrid'){
hybridAddressing = true;
}
let segmentString = ''
let curlString = ''
let haString = ''
let haCommandCurlString = '';
let colorSeparatorStart = '\'';
let colorSeparatorEnd = '\'';
if (!hexValueCheck){
colorSeparatorStart = '[';
colorSeparatorEnd = ']';
}
// Warnings
let hasTransparency = false; //If alpha < 255 is detected on any pixel, this is set to true in code below
let imageInfo = '';
// Create an off-screen canvas
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
// Create an image element and set its src to the base64 image
var image = new Image();
image.src = base64Image;
// Wait for the image to load before drawing it onto the canvas
image.onload = function() {
let scalePath = document.getElementById("scalePath");
let color = scalePath.getAttribute("fill");
let sizeX = document.getElementById("sizeX").value;
let sizeY = document.getElementById("sizeY").value;
if (color != accentColor || sizeX < 1 || sizeY < 1){
//image will not be rezised Set desitred size to original size
sizeX = image.width;
sizeY = image.height;
}
// Set the canvas size to the same as the desired image size
canvas.width = sizeX;
canvas.height = sizeY;
imageInfo = '<p>Width: ' + sizeX + ', Height: ' + sizeY + ' (make sure this matches your led matrix setup)</p>'
// Draw the image onto the canvas
context.drawImage(image, 0, 0, sizeX, sizeY);
// Get the pixel data from the canvas
var pixelData = context.getImageData(0, 0, sizeX, sizeY).data;
// Create an array to hold the RGB values of each pixel
var pixelRGBValues = [];
// If the first row of the led matrix is right -> left
let right2leftAdjust = 1;
if (ledSetupSelection == 'l2r'){
right2leftAdjust = 0;
}
// Loop through the pixel data and get the RGB values of each pixel
for (var i = 0; i < pixelData.length; i += 4) {
var r = pixelData[i];
var g = pixelData[i + 1];
var b = pixelData[i + 2];
var a = pixelData[i + 3];
let pixel = i/4
let row = Math.floor(pixel/sizeX);
let led = pixel;
if (ledSetupSelection == 'matrix'){
//Do nothing, the matrix is set upp like the index in the image
//Every row starts from the left, i.e. no zigzagging
}
else if ((row + right2leftAdjust) % 2 === 0) {
//Setup is traditional zigzag
//right2leftAdjust basically flips the row order if = 1
//Row is left to right
//Leave led index as pixel index
} else {
//Setup is traditional zigzag
//Row is right to left
//Invert index of row for led
let indexOnRow = led - (row * sizeX);
let maxIndexOnRow = sizeX - 1;
let reversedIndexOnRow = maxIndexOnRow - indexOnRow;
led = (row * sizeX) + reversedIndexOnRow;
}
// Add the RGB values to the pixel RGB values array
pixelRGBValues.push([r, g, b, a, led, pixel, row]);
}
pixelRGBValues.sort((a, b) => a[5] - b[5]);
//Copy the values to a new array for resorting
let ledRGBValues = [... pixelRGBValues];
//Sort the array based on led index
ledRGBValues.sort((a, b) => a[4] - b[4]);
//Generate JSON in WLED format
let JSONledString = '';
let JSONledStringShort = '';
//Set starting values for the segment check to something that is no color
let segmentStart = -1;
let maxi = ledRGBValues.length;
let curentColorIndex = 0
let commandArray = [];
//For evry pixel in the LED array
for (let i = 0; i < maxi; i++) {
let pixel = ledRGBValues[i];
let r = pixel[0];
let g = pixel[1];
let b = pixel[2];
let a = pixel[3];
let segmentString = '';
let segmentEnd = -1;
if(segmentValueCheck){
if (segmentStart < 0){
//This is the first led of a new segment
segmentStart = i;
} //Else we allready have a start index
if (i < maxi - 1){
let iNext = i + 1;
let nextPixel = ledRGBValues[iNext];
if (nextPixel[0] != r || nextPixel[1] != g || nextPixel[2] != b ){
//Next pixel has new color
//The current segment ends with this pixel
segmentEnd = i + 1 //WLED wants the NEXT LED as the stop led...
if (segmentStart == i && hybridAddressing){
//If only one led/pixel, no segment info needed
if (JSONledString == ''){
//If addressing is single, we need to start every command with a starting possition
segmentString = '' + i + ',';
//Fixed to b2
} else{
segmentString = ''
}
}
else {
segmentString = segmentStart + ',' + segmentEnd + ',';
}
}
} else {
//This is the last pixel, so the segment must end
segmentEnd = i + 1;
if (segmentStart + 1 == segmentEnd && hybridAddressing){
//If only one led/pixel, no segment info needed
if (JSONledString == ''){
//If addressing is single, we need to start every command with a starting possition
segmentString = '' + i + ',';
//Fixed to b2
} else{
segmentString = ''
}
}
else {
segmentString = segmentStart + ',' + segmentEnd + ',';
}
}
} else{
//Write every pixel
if (JSONledString == ''){
//If addressing is single, we need to start every command with a starting possition
JSONledString = i
//Fixed to b2
}
segmentStart = i
segmentEnd = i
//Segment string should be empty for when addressing single. So no need to set it again.
}
if (a < 255){
hasTransparency = true; //If ANY pixel has alpha < 255 then this is set to true to warn the user
}
if (segmentEnd > -1){
//This is the last pixel in the segment, write to the JSONledString
//Return color value in selected format
let colorValueString = r + ',' + g + ',' + b ;
if (hexValueCheck){
const [red, green, blue] = [r, g, b];
colorValueString = `${[red, green, blue].map(x => x.toString(16).padStart(2, '0')).join('')}`;
} else{
//do nothing, allready set
}
// Check if start and end is the same, in which case remove
JSONledString = JSONledString + segmentString + colorSeparatorStart + colorValueString + colorSeparatorEnd;
fileJSON = JSONledString + segmentString + colorSeparatorStart + colorValueString + colorSeparatorEnd;
curentColorIndex = curentColorIndex + 1; // We've just added a new color to the string so up the count with one
if (curentColorIndex % maxNoOfColorsInCommandSting === 0 || i == maxi - 1) {
//If we have accumulated the max number of colors to send in a single command or if this is the last pixel, we should write the current colorstring to the array
commandArray.push(JSONledString);
JSONledString = ''; //Start on an new command string
} else
{
//Add a comma to continue the command string
JSONledString = JSONledString + ','
}
//Reset segment values
segmentStart = - 1;
}
}
JSONledString = ''
//For evry commandString in the array
for (let i = 0; i < commandArray.length; i++) {
let thisJSONledString = JSONledStringStart + document.getElementById('brightnessNumber').value + JSONledStringMid1 + document.getElementById('targetSegment').value + JSONledStringMid2 + commandArray[i] + JSONledStringEnd;
httpArray.push(thisJSONledString);
let thiscurlString = curlStart + document.getElementById('curlUrl').value + curlMid1 + thisJSONledString + curlEnd;
//Aggregated Strings That should be returned to the user
if (i > 0){
JSONledString = JSONledString + '\n';
curlString = curlString + ' && ';
}
JSONledString = JSONledString + thisJSONledString;
curlString = curlString + thiscurlString;
}
haString = haStart + document.getElementById('haID').value + haMid1 + document.getElementById('haName').value + haMid2 + document.getElementById('haUID').value + haMid3 +curlString + haMid3 + document.getElementById('curlUrl').value + haEnd;
if (formatSelection == 'wled'){
JSONled.value = JSONledString;
} else if (formatSelection == 'curl'){
JSONled.value = curlString;
} else if (formatSelection == 'ha'){
JSONled.value = haString;
} else {
JSONled.value = 'ERROR!/n' + formatSelection + ' is an unknown format.'
}
fileJSON = fileJSON + JSONledStringEnd;
let infoDiv = document.getElementById('image-info');
let canvasDiv = document.getElementById('image-info');
if (hasTransparency){
imageInfo = imageInfo + '<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>'
}
infoDiv.innerHTML = imageInfo;
canvasDiv.style.display = "block"
//Drawing the image
drawBoxes(pixelRGBValues, sizeX, sizeY);
}
}
================================================
FILE: html/index.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 Converter</title>
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
</head>
<body>
<div class="top-part" >
<div style="display: flex; justify-content: center;">
<h1 style="display: flex; align-items: center;">
<svg style="width:36px;height:36px" id="logomatrix" viewBox="0 0 24 24">
<path fill="#7e4c80" d="M6,5
a2,2 0 1,0 4,0
a2,2 0 1,0 -4,0" id="sc1"/>
<path fill="#7e4c80" d="M12,5
a2,2 0 1,0 4,0
a2,2 0 1,0 -4,0" id="sc2"/>
<path fill="#7e4c80" d="M18,5
a2,2 0 1,0 4,0
a2,2 0 1,0 -4,0" id="sc3"/>
<path fill="#7e4c80" d="M6,11
a2,2 0 1,0 4,0
a2,2 0 1,0 -4,0" id="sc4"/>
<path fill="#7e4c80" d="M12,11
a2,2 0 1,0 4,0
a2,2 0 1,0 -4,0" id="sc5"/>
<path fill="#7e4c80" d="M18,11
a2,2 0 1,0 4,0
a2,2 0 1,0 -4,0" id="sc6"/>
<path fill="#7e4c80" d="M6,17
a2,2 0 1,0 4,0
a2,2 0 1,0 -4,0" id="sc7"/>
<path fill="#7e4c80" d="M12,17
a2,2 0 1,0 4,0
a2,2 0 1,0 -4,0" id="sc8"/>
<path fill="#7e4c80" d="M18,17
a2,2 0 1,0 4,0
a2,2 0 1,0 -4,0" id="sc9"/>
</svg>
Led Matrix Pixel Art Converter
</h1>
</div>
<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="hybrid" selected>Hybrid (#f0f0f0,10, 17, #f4f4f4)</option>
<option value="range">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="255">
<span id="brightnessValue">255</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 class="fullTextField" 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 class="fullTextField" 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 class="fullTextField" 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 class="fullTextField" type="text" id="curlUrl" value="">
</td>
</tr>
<tr>
<td style="vertical-align: middle;">
<label for="targetSegment">Target segment id:</label>
</td>
<td style="vertical-align: middle;">
<select id="targetSegment">
</select>
</td>
</tr>
</table>
<table class= "scaleTableClass" id="scaleTable" style="width: 100%; table-layout: fixed; align-content: center;">
<tr>
<td style="vertical-align: middle;">
<div id="scaleDiv">
<svg id="scaleToggle" style="width:36px;height:36px" viewBox="0 0 24 24" onclick="switchScale()">
<path id="scaleTogglePath" fill="currentColor" d="M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M7,15A3,3 0 0,1 4,12A3,3 0 0,1 7,9A3,3 0 0,1 10,12A3,3 0 0,1 7,15Z" />
</svg>
<svg id="scaleSvg" style="width:36px;height:36px" viewBox="0 0 24 24" onclick="switchScale()">
<path id="scalePath" fill="currentColor" d="M21,15H23V17H21V15M21,11H23V13H21V11M23,19H21V21C22,21 23,20 23,19M13,3H15V5H13V3M21,7H23V9H21V7M21,3V5H23C23,4 22,3 21,3M1,7H3V9H1V7M17,3H19V5H17V3M17,19H19V21H17V19M3,3C2,3 1,4 1,5H3V3M9,3H11V5H9V3M5,3H7V5H5V3M1,11V19A2,2 0 0,0 3,21H15V11H1M3,19L5.5,15.79L7.29,17.94L9.79,14.72L13,19H3Z" />
</svg>
</div>
</td>
<td style="vertical-align: middle;">
<div id="sizeDiv" style="display: none;">
<label for="sizeX">X : </label> <input class="sizeInputFields" type="number" id="sizeX" value="0">
<label for="sizeY">Y : </label> <input class="sizeInputFields" type="number" id="sizeY" value="0">
</div>
</td>
</tr>
</table>
</p>
<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;">
<img id="newimage" style="display: block; margin: 0 auto;"><br>
</div>
<div id="submitConvertDiv" style="display: none;">
<button id="convertbutton" class="buttonclass"></button>
</div>
<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>
<textarea id="JSONled"></textarea>
</div>
<div id="button-container" style="display: none;">
<button id="copyJSONledbutton" class="buttonclass"></button>
<div id="gap1" class="gap"></div>
<button id="sendJSONledbutton" class="buttonclass"></button>
<div id = "gap2" class="gap"></div>
<button id="fileJSONledbutton" class="buttonclass"></button>
</div>
<div>
<h3><div id="version">Version 1.0.4</div> - <a href="https://github.com/werkstrom/WLED-PixelArtConverter/blob/main/README.md" target="_blank">Help/About</a></h3>
</div>
</div>
<div id=bottom-part style="display: none" class=bottom-part></div>
<canvas id="pixelCanvas"></canvas>
</div>
<script>
function loadFiles(fileNames, index) {
if (index === fileNames.length) {
return;
}
var fileName = fileNames[index];
var fileExt = fileName.split('.').pop();
var element;
if (fileExt === 'js') {
element = document.createElement('script');
element.type = 'text/javascript';
element.src = fileName + '?time=' + new Date().getTime();
} else if (fileExt === 'css') {
element = document.createElement('link');
element.rel = 'stylesheet';
element.href = fileName + '?time=' + new Date().getTime();
}
element.onload = function() {
loadFiles(fileNames, index + 1);
}
document.getElementsByTagName('head')[0].appendChild(element);
}
var files = ["statics.js", "getPixelValues.js", "boxdraw.js", "index.js", "styles.css"];
loadFiles(files, 0);
</script>
</body>
</html>
================================================
FILE: html/index.js
================================================
//Start up code
document.getElementById('curlUrl').value = location.host;
let devMode = false;
const urlParams = new URLSearchParams(window.location.search);
if(urlParams.has('dev')){
devMode = true;
}
if(devMode){
console.log('Developer mode active. Experimental and unstable functions active.')
} else{
console.log('Developer mode inactive. Append "?dev" to the URL.')
}
if(devMode){
document.getElementById("fileJSONledbutton").style.display = 'buttonclass'
document.getElementById("gap2").style.display = 'gap'
} else {
document.getElementById("fileJSONledbutton").style.display = 'none'
document.getElementById("gap2").style.display = 'none'
}
let httpArray = [];
let fileJSON = '';
//On submit button pressed =======================
document.getElementById("convertbutton").addEventListener("click", function() {
let base64Image = document.getElementById('preview').src;
if (isValidBase64Gif(base64Image)) {
document.getElementById('image').src = base64Image;
getPixelRGBValues(base64Image);
document.getElementById('image-container').style.display = "block"
document.getElementById("button-container").style.display = "";
}
else {
let infoDiv = document.getElementById('image-info');
let imageInfo = '<p><b>WARNING!</b> File does not appear to be a valid image</p>'
infoDiv.innerHTML = imageInfo;
infoDiv.style.display = "block"
document.getElementById('image-container').style.display = "none";
document.getElementById('JSONled').value = '';
console.log("The string '" + base64Image + "' is not a valid base64 image.");
}
});
// Code for copying the generated string to clipboard
copyJSONledbutton.addEventListener('click', async () => {
let JSONled = document.getElementById('JSONled');
JSONled.select();
try {
await navigator.clipboard.writeText(JSONled.value);
} catch (err) {
try {
await document.execCommand("copy");
} catch (err) {
console.error('Failed to copy text: ', err);
}
}
});
sendJSONledbutton.addEventListener('click', async () => {
if (window.location.protocol === "https:") {
alert('Will only be available when served over http (or WLED is run over https)');
} else {
postPixels();
}
});
fileJSONledbutton.addEventListener('click', async () => {
if (window.location.protocol === "https:") {
alert('Will only be available when served over http (or WLED is run over https)');
} else {
let JSONFileName = 'TheName.json'
let urlString = 'http://'+document.getElementById('curlUrl').value+'/upload'
sendAsFile(fileJSON, JSONFileName, urlString);
}
});
async function postPixels() {
for (let i of httpArray) {
try {
console.log(i);
console.log(i.length);
const response = await fetch('http://'+document.getElementById('curlUrl').value+'/json/state', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
//'Content-Type': 'text/html; charset=UTF-8'
},
body: i
});
const data = await response.json();
console.log(data);
} catch (error) {
console.error(error);
}
}
}
//File uploader code
const dropZone = document.getElementById('drop-zone');
const filePicker = document.getElementById('file-picker');
const preview = document.getElementById('preview');
// Listen for dragenter, dragover, and drop events
dropZone.addEventListener('dragenter', dragEnter);
dropZone.addEventListener('dragover', dragOver);
dropZone.addEventListener('drop', dropped);
dropZone.addEventListener('click', zoneClicked);
// Listen for change event on file picker
filePicker.addEventListener('change', filePicked);
// Handle zone click
function zoneClicked(e) {
e.preventDefault();
//this.classList.add('drag-over');
//alert('Hej');
filePicker.click();
}
// Handle dragenter
function dragEnter(e) {
e.preventDefault();
this.classList.add('drag-over');
}
// Handle dragover
function dragOver(e) {
e.preventDefault();
}
// Handle drop
function dropped(e) {
e.preventDefault();
this.classList.remove('drag-over');
// Get the dropped file
const file = e.dataTransfer.files[0];
updatePreview(file);
}
// Handle file picked
function filePicked(e) {
// Get the picked file
const file = e.target.files[0];
updatePreview(file);
}
// Update the preview image
function updatePreview(file) {
// Use FileReader to read the file
const reader = new FileReader();
reader.onload = function() {
// Update the preview image
preview.src = reader.result;
document.getElementById("submitConvertDiv").style.display = "";
};
reader.readAsDataURL(file);
}
function isValidBase64Gif(string) {
// Use a regular expression to check that the string is a valid base64 string
/*
const base64gifPattern = /^data:image\/gif;base64,([A-Za-z0-9+/:]{4})*([A-Za-z0-9+/:]{3}=|[A-Za-z0-9+/:]{2}==)?$/;
const base64pngPattern = /^data:image\/png;base64,([A-Za-z0-9+/:]{4})*([A-Za-z0-9+/:]{3}=|[A-Za-z0-9+/:]{2}==)?$/;
const base64jpgPattern = /^data:image\/jpg;base64,([A-Za-z0-9+/:]{4})*([A-Za-z0-9+/:]{3}=|[A-Za-z0-9+/:]{2}==)?$/;
const base64webpPattern = /^data:image\/webp;base64,([A-Za-z0-9+/:]{4})*([A-Za-z0-9+/:]{3}=|[A-Za-z0-9+/:]{2}==)?$/;
*/
//REMOVED, Any image appear to work as long as it can be drawn to the canvas. Leaving code in for future use, possibly
if (1==1 || base64gifPattern.test(string) || base64pngPattern.test(string) || base64jpgPattern.test(string) || base64webpPattern.test(string)) {
return true;
}
else {
//Not OK
return false;
}
}
document.getElementById("brightnessNumber").oninput = function() {
document.getElementById("brightnessValue").textContent = this.value;
}
document.getElementById("colorLimitNumber").oninput = function() {
document.getElementById("colorLimitValue").textContent = this.value;
}
var formatSelector = document.getElementById("formatSelector");
var hideableRows = document.querySelectorAll(".ha-hide");
for (var i = 0; i < hideableRows.length; i++) {
hideableRows[i].classList.add("hide");
}
formatSelector.addEventListener("change", function() {
for (var i = 0; i < hideableRows.length; i++) {
hideableRows[i].classList.toggle("hide", this.value !== "ha");
}
});
function switchScale() {
let scalePath = document.getElementById("scalePath");
let scaleTogglePath = document.getElementById("scaleTogglePath");
let color = scalePath.getAttribute("fill");
let d = ''
if (color === accentColor) {
color = accentTextColor;
d = scaleToggleOffd
document.getElementById("sizeDiv").style.display = "none";
// Set values to actual XY of image, if possible
} else {
color = accentColor;
d = scaleToggleOnd
document.getElementById("sizeDiv").style.display = "";
}
scalePath.setAttribute("fill", color);
scaleTogglePath.setAttribute("fill", color);
scaleTogglePath.setAttribute("d", d);
}
function sendAsFile(jsonStringInput, fileName, urlString) {
//var jsonString = JSON.stringify({name: "value"});
var file = new Blob([jsonStringInput], {type: 'application/json'});
console.log(jsonStringInput)
console.log(fileName);
console.log(urlString);
var formData = new FormData();
formData.append('file', file, fileName);
var xhr = new XMLHttpRequest();
xhr.open('POST', urlString, true);
xhr.onload = function() {
if (xhr.status === 200) {
console.log('File uploaded successfully!');
} else {
console.log('File upload failed!');
}
};
xhr.send(formData);
}
function generateSegmentOptions(array) {
//This function is prepared for a name property on each segment for easier selection
//Currently the name is generated generically based on index
var select = document.getElementById("targetSegment");
select.innerHTML = "";
for (var i = 0; i < array.length; i++) {
var option = document.createElement("option");
option.value = array[i].value;
option.text = array[i].text;
select.appendChild(option);
if(i === 0) {
option.selected = true;
}
}
}
//Initial population of segment selection
function generateSegmentArray(noOfSegments) {
var arr = [];
for (var i = 0; i < noOfSegments; i++) {
arr.push({
value: i,
text: "Segment index " + i
});
}
return arr;
}
//Animate matrix
var matrixcircles = document.querySelectorAll("#logomatrix path");
var intervalId;
// Function that changes the color of a random circle to a lighter purple
function changeColorOfDot() {
// Get a random number between 0 and the number of circles
var randomIndex = Math.floor(Math.random() * matrixcircles.length);
// Get the circle at the random index
var randomCircle = matrixcircles[randomIndex];
// Store the current fill color
var currentColor = randomCircle.getAttribute("fill");
// Change the color of the circle
randomCircle.setAttribute("fill", "#bb8fbc");
setTimeout(() => {
randomCircle.setAttribute("fill", currentColor);
clearInterval(intervalId);
intervalId = setInterval(changeColorOfDot, randomInterval());
}, 500);
}
function randomInterval() {
var interval = Math.floor(Math.random() * (5 - 1 + 1)) + 1;
return interval * 1000;
}
// call the function changeColorOfDot every 10 seconds
intervalId = setInterval(changeColorOfDot, randomInterval());
var segmentData = generateSegmentArray(10);
generateSegmentOptions(segmentData);
document.getElementById("fileJSONledbutton").innerHTML =
'<svg style="width:36px;height:36px" viewBox="0 0 24 24"><path fill="currentColor" d="M20 18H4V8H20M20 6H12L10 4H4A2 2 0 0 0 2 6V18A2 2 0 0 0 4 20H20A2 2 0 0 0 22 18V8A2 2 0 0 0 20 6M16 17H14V13H11L15 9L19 13H16Z" /></svg> File to device'
document.getElementById("convertbutton").innerHTML =
'<svg style="width:36px;height:36px" viewBox="0 0 24 24"><path fill="currentColor" d="M12,6V9L16,5L12,1V4A8,8 0 0,0 4,12C4,13.57 4.46,15.03 5.24,16.26L6.7,14.8C6.25,13.97 6,13 6,12A6,6 0 0,1 12,6M18.76,7.74L17.3,9.2C17.74,10.04 18,11 18,12A6,6 0 0,1 12,18V15L8,19L12,23V20A8,8 0 0,0 20,12C20,10.43 19.54,8.97 18.76,7.74Z" /> </svg> Convert to WLED JSON ';
document.getElementById("copyJSONledbutton").innerHTML =
'<svg class="svg-icon" style="width:36px;height:36px" viewBox="0 0 24 24"> <path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z" /> </svg> Copy to clipboard';
document.getElementById("sendJSONledbutton").innerHTML =
'<svg class="svg-icon" style="width:36px;height:36px" viewBox="0 0 24 24"> <path fill="currentColor" d="M6.5 20Q4.22 20 2.61 18.43 1 16.85 1 14.58 1 12.63 2.17 11.1 3.35 9.57 5.25 9.15 5.88 6.85 7.75 5.43 9.63 4 12 4 14.93 4 16.96 6.04 19 8.07 19 11 20.73 11.2 21.86 12.5 23 13.78 23 15.5 23 17.38 21.69 18.69 20.38 20 18.5 20H13Q12.18 20 11.59 19.41 11 18.83 11 18V12.85L9.4 14.4L8 13L12 9L16 13L14.6 14.4L13 12.85V18H18.5Q19.55 18 20.27 17.27 21 16.55 21 15.5 21 14.45 20.27 13.73 19.55 13 18.5 13H17V11Q17 8.93 15.54 7.46 14.08 6 12 6 9.93 6 8.46 7.46 7 8.93 7 11H6.5Q5.05 11 4.03 12.03 3 13.05 3 14.5 3 15.95 4.03 17 5.05 18 6.5 18H9V20M12 13Z" /> </svg> Send to device';
================================================
FILE: html/pixartmin.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 Converter</title>
<style>
h1,h2{line-height:.5;margin:1px 0}h1,h2,h3{margin:1px 0}#fieldTable,#scaleTable,p{color:#777;font-family:Arcade,Arial,sans-serif}#drop-zone,.container{text-align:center;padding:20px}#drop-zone,#fieldTable,#scaleTable,body,h1,h2,h3,p{font-family:Arcade,Arial,sans-serif}.rangeNumber,.svg-icon{vertical-align:middle}a:active,a:link,a:visited,h2,h3{color:rgba(126,76,128,.61)}a:hover,h1{color:#7e4c80}a:active,a:hover,a:link,a:visited{background-color:transparent;text-decoration:none}.box{border:2px solid #fff}body{background-color:#151515}.top-part{width:600px;margin:0 auto}.container{max-width:100% -40px;border-radius:0}h1{font-size:2.3em}h2{font-size:1.1em;text-align:center}h3{font-size:.7em;line-height:1.4;text-align:center;align-items:center;justify-content:center;display:flex}p{font-size:1.2em;line-height:1.5}#fieldTable,#scaleTable{font-size:1 em;line-height:1}#drop-zone{display:block;width:100%-40px;border:3px dashed #7e4c80;border-radius:0;margin:0;cursor:pointer;font-size:15px;color:#777}* select,.fullTextField[type=text]{background-color:#333;border:1px solid silver;width:100%;color:#777;font-size:15px}#file-picker,.hide{display:none}* select{margin-top:.5em;margin-bottom:.5em;padding:0;height:27px;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}.fullTextField[type=text]{padding-inline-start:5px;margin-top:10px;height:24px;border-radius:0;font-family:Arcade,Arial,sans-serif}* button,* input[type=submit]{background-color:#333;border:1px solid silver;width:100%;font-family:Arcade,Arial,sans-serif}* input[type=submit]{padding:.5em;border-radius:0;font-size:1.3em;color:#777}* button{padding-inline:5px;border-radius:0;font-size:1em;color:#777;display:flex;align-items:center;justify-content:center;cursor:pointer}.sizeInputFields,textarea{background-color:#333;border:1px solid silver;color:#777}textarea{grid-row:1/2;width:100%;height:200px}.grids-class{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}.buttondivmid-class,.gap{width:10px}#sizeDiv *,.buttondiv-class,.buttondivmid-class{display:inline-block}.buttondiv-class{flex:1}#image-container{display:grid;grid-template-rows:1fr 1fr}#button-container{display:flex;padding-bottom:10px;padding-top:10px}.buttonclass{flex:1;padding-top:5px;padding-bottom:5px}#submitConvert::before{content:"";display:inline-block;background-image:url('data:image/svg+xml;utf8, <svg style="width:24px;height:24px" viewBox="0 0 24 24" <path fill="currentColor" d="M12,6V9L16,5L12,1V4A8,8 0 0,0 4,12C4,13.57 4.46,15.03 5.24,16.26L6.7,14.8C6.25,13.97 6,13 6,12A6,6 0 0,1 12,6M18.76,7.74L17.3,9.2C17.74,10.04 18,11 18,12A6,6 0 0,1 12,18V15L8,19L12,23V20A8,8 0 0,0 20,12C20,10.43 19.54,8.97 18.76,7.74Z" /></svg>');width:36px;height:36px}.sizeInputFields{width:50px;padding-inline-start:5px;margin-top:-5px;height:24px;border-radius:0;font-family:Arcade,Arial,sans-serif;font-size:15px}
</style>
</head>
<body>
<body>
<div class="top-part" >
<div style="display: flex; justify-content: center;">
<h1 style="display: flex; align-items: center;">
<svg style="width:36px;height:36px" id="logomatrix" viewBox="0 0 24 24">
<path fill="#7e4c80" d="M6,5
a2,2 0 1,0 4,0
a2,2 0 1,0 -4,0" id="sc1"/>
<path fill="#7e4c80" d="M12,5
a2,2 0 1,0 4,0
a2,2 0 1,0 -4,0" id="sc2"/>
<path fill="#7e4c80" d="M18,5
a2,2 0 1,0 4,0
a2,2 0 1,0 -4,0" id="sc3"/>
<path fill="#7e4c80" d="M6,11
a2,2 0 1,0 4,0
a2,2 0 1,0 -4,0" id="sc4"/>
<path fill="#7e4c80" d="M12,11
a2,2 0 1,0 4,0
a2,2 0 1,0 -4,0" id="sc5"/>
<path fill="#7e4c80" d="M18,11
a2,2 0 1,0 4,0
a2,2 0 1,0 -4,0" id="sc6"/>
<path fill="#7e4c80" d="M6,17
a2,2 0 1,0 4,0
a2,2 0 1,0 -4,0" id="sc7"/>
<path fill="#7e4c80" d="M12,17
a2,2 0 1,0 4,0
a2,2 0 1,0 -4,0" id="sc8"/>
<path fill="#7e4c80" d="M18,17
a2,2 0 1,0 4,0
a2,2 0 1,0 -4,0" id="sc9"/>
</svg>
Led Matrix Pixel Art Converter
</h1>
</div>
<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="hybrid" selected>Hybrid (#f0f0f0,10, 17, #f4f4f4)</option>
<option value="range">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="255">
<span id="brightnessValue">255</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 class="fullTextField" 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 class="fullTextField" 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 class="fullTextField" 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 class="fullTextField" type="text" id="curlUrl" value="">
</td>
</tr>
<tr>
<td style="vertical-align: middle;">
<label for="targetSegment">Target segment id:</label>
</td>
<td style="vertical-align: middle;">
<select id="targetSegment">
</select>
</td>
</tr>
</table>
<table class= "scaleTableClass" id="scaleTable" style="width: 100%; table-layout: fixed; align-content: center;">
<tr>
<td style="vertical-align: middle;">
<div id="scaleDiv">
<svg id="scaleToggle" style="width:36px;height:36px" viewBox="0 0 24 24" onclick="switchScale()">
<path id="scaleTogglePath" fill="currentColor" d="M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M7,15A3,3 0 0,1 4,12A3,3 0 0,1 7,9A3,3 0 0,1 10,12A3,3 0 0,1 7,15Z" />
</svg>
<svg id="scaleSvg" style="width:36px;height:36px" viewBox="0 0 24 24" onclick="switchScale()">
<path id="scalePath" fill="currentColor" d="M21,15H23V17H21V15M21,11H23V13H21V11M23,19H21V21C22,21 23,20 23,19M13,3H15V5H13V3M21,7H23V9H21V7M21,3V5H23C23,4 22,3 21,3M1,7H3V9H1V7M17,3H19V5H17V3M17,19H19V21H17V19M3,3C2,3 1,4 1,5H3V3M9,3H11V5H9V3M5,3H7V5H5V3M1,11V19A2,2 0 0,0 3,21H15V11H1M3,19L5.5,15.79L7.29,17.94L9.79,14.72L13,19H3Z" />
</svg>
</div>
</td>
<td style="vertical-align: middle;">
<div id="sizeDiv" style="display: none;">
<label for="sizeX">X : </label> <input class="sizeInputFields" type="number" id="sizeX" value="0">
<label for="sizeY">Y : </label> <input class="sizeInputFields" type="number" id="sizeY" value="0">
</div>
</td>
</tr>
</table>
</p>
<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;">
<img id="newimage" style="display: block; margin: 0 auto;"><br>
</div>
<div id="submitConvertDiv" style="display: none;">
<button id="convertbutton" class="buttonclass"></button>
</div>
<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>
<textarea id="JSONled"></textarea>
</div>
<div id="button-container" style="display: none;">
<button id="copyJSONledbutton" class="buttonclass"></button>
<div id="gap1" class="gap"></div>
<button id="sendJSONledbutton" class="buttonclass"></button>
<div id = "gap2" class="gap"></div>
<button id="fileJSONledbutton" class="buttonclass"></button>
</div>
<div>
<h3><div id="version">Version 1.0.4</div> - <a href="https://github.com/werkstrom/WLED-PixelArtConverter/blob/main/README.md" target="_blank">Help/About</a></h3>
</div>
</div>
<div id=bottom-part style="display: none" class=bottom-part></div>
<canvas id="pixelCanvas"></canvas>
</div>
<script>
var intervalId,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":{"id":',JSONledStringMid2=',"i":[',JSONledStringEnd="]}}";var accentColor="#7E4C80",accentTextColor="#777",scaleToggleOffd="M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M7,15A3,3 0 0,1 4,12A3,3 0 0,1 7,9A3,3 0 0,1 10,12A3,3 0 0,1 7,15Z",scaleToggleOnd="M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M17,15A3,3 0 0,1 14,12A3,3 0 0,1 17,9A3,3 0 0,1 20,12A3,3 0 0,1 17,15Z";document.getElementById("curlUrl").value=location.host;let devMode=!1;const urlParams=new URLSearchParams(window.location.search);urlParams.has("dev")&&(devMode=!0),devMode?console.log("Developer mode active. Experimental and unstable functions active."):console.log('Developer mode inactive. Append "?dev" to the URL.'),devMode?(document.getElementById("fileJSONledbutton").style.display="buttonclass",document.getElementById("gap2").style.display="gap"):(document.getElementById("fileJSONledbutton").style.display="none",document.getElementById("gap2").style.display="none");let httpArray=[],fileJSON="";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}),l=await t.json();console.log(l)}catch(n){console.error(n)}}document.getElementById("convertbutton").addEventListener("click",function(){let e=document.getElementById("preview").src;if(isValidBase64Gif(e))document.getElementById("image").src=e,getPixelRGBValues(e),document.getElementById("image-container").style.display="block",document.getElementById("button-container").style.display="";else{let t=document.getElementById("image-info");t.innerHTML="<p><b>WARNING!</b> File does not appear to be a valid image</p>",t.style.display="block",document.getElementById("image-container").style.display="none",document.getElementById("JSONled").value="",console.log("The string '"+e+"' is not a valid base64 image.")}}),copyJSONledbutton.addEventListener("click",async()=>{let e=document.getElementById("JSONled");e.select();try{await navigator.clipboard.writeText(e.value)}catch(t){try{await document.execCommand("copy")}catch(l){console.error("Failed to copy text: ",l)}}}),sendJSONledbutton.addEventListener("click",async()=>{"https:"===window.location.protocol?alert("Will only be available when served over http (or WLED is run over https)"):postPixels()}),fileJSONledbutton.addEventListener("click",async()=>{if("https:"===window.location.protocol)alert("Will only be available when served over http (or WLED is run over https)");else{let e;sendAsFile(fileJSON,"TheName.json","http://"+document.getElementById("curlUrl").value+"/upload")}});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("submitConvertDiv").style.display=""},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 switchScale(){let e=document.getElementById("scalePath"),t=document.getElementById("scaleTogglePath"),l=e.getAttribute("fill"),n="";l===accentColor?(l=accentTextColor,n=scaleToggleOffd,document.getElementById("sizeDiv").style.display="none"):(l=accentColor,n=scaleToggleOnd,document.getElementById("sizeDiv").style.display=""),e.setAttribute("fill",l),t.setAttribute("fill",l),t.setAttribute("d",n)}function sendAsFile(e,t,l){var n=new Blob([e],{type:"application/json"});console.log(e),console.log(t),console.log(l);var a=new FormData;a.append("file",n,t);var o=new XMLHttpRequest;o.open("POST",l,!0),o.onload=function(){200===o.status?console.log("File uploaded successfully!"):console.log("File upload failed!")},o.send(a)}function generateSegmentOptions(e){var t=document.getElementById("targetSegment");t.innerHTML="";for(var l=0;l<e.length;l++){var n=document.createElement("option");n.value=e[l].value,n.text=e[l].text,t.appendChild(n),0===l&&(n.selected=!0)}}function generateSegmentArray(e){for(var t=[],l=0;l<e;l++)t.push({value:l,text:"Segment index "+l});return t}formatSelector.addEventListener("change",function(){for(var e=0;e<hideableRows.length;e++)hideableRows[e].classList.toggle("hide","ha"!==this.value)});var matrixcircles=document.querySelectorAll("#logomatrix path");function changeColorOfDot(){var e=matrixcircles[Math.floor(Math.random()*matrixcircles.length)],t=e.getAttribute("fill");e.setAttribute("fill","#bb8fbc"),setTimeout(()=>{e.setAttribute("fill",t),clearInterval(intervalId),intervalId=setInterval(changeColorOfDot,randomInterval())},500)}function randomInterval(){return 1e3*(Math.floor(5*Math.random())+1)}intervalId=setInterval(changeColorOfDot,randomInterval());var segmentData=generateSegmentArray(10);function getPixelRGBValues(e){httpArray=[],fileJSON=JSONledStringStart+document.getElementById("brightnessNumber").value+JSONledStringMid1+document.getElementById("targetSegment").value+JSONledStringMid2;let t=document.getElementById("JSONled"),l=document.getElementById("colorLimitNumber").value,n=!1,a=-1,o=document.getElementById("formatSelector");a=o.selectedIndex;let r=o.options[a].value;a=(o=document.getElementById("ledSetupSelector")).selectedIndex;let d=o.options[a].value;a=(o=document.getElementById("colorFormatSelector")).selectedIndex;let s=!0;"dec"==o.options[a].value&&(s=!1),a=(o=document.getElementById("addressingSelector")).selectedIndex;let _=!0;"single"==o.options[a].value?_=!1:"hybrid"==o.options[a].value&&(n=!0);let c="",g="",$="'",u="'";s||($="[",u="]");let m=!1,p="";var y=document.createElement("canvas"),v=y.getContext("2d"),h=new Image;h.src=e,h.onload=function(){let e=document.getElementById("scalePath").getAttribute("fill"),a=document.getElementById("sizeX").value,o=document.getElementById("sizeY").value;(e!=accentColor||a<1||o<1)&&(a=h.width,o=h.height),y.width=a,y.height=o,p="<p>Width: "+a+", Height: "+o+" (make sure this matches your led matrix setup)</p>",v.drawImage(h,0,0,a,o);var f=v.getImageData(0,0,a,o).data,E=[];let I=1;"l2r"==d&&(I=0);for(var B=0;B<f.length;B+=4){var S=f[B],b=f[B+1],A=f[B+2],w=f[B+3];let x=B/4,L=Math.floor(x/a),C=x;if("matrix"==d);else if((L+I)%2==0);else{let O=C-L*a,M=a-1-O;C=L*a+M}E.push([S,b,A,w,C,x,L])}E.sort((e,t)=>e[5]-t[5]);let H=[...E];H.sort((e,t)=>e[4]-t[4]);let N="",T=-1,k=H.length,J=0,V=[];for(let D=0;D<k;D++){let P=H[D],R=P[0],j=P[1],W=P[2],Z=P[3],U="",z=-1;if(_){if(T<0&&(T=D),D<k-1){let F=H[D+1];(F[0]!=R||F[1]!=j||F[2]!=W)&&(z=D+1,U=T==D&&n?""==N?""+D+",":"":T+","+z+",")}else U=T+1==(z=D+1)&&n?""==N?""+D+",":"":T+","+z+","}else""==N&&(N=D),T=D,z=D;if(Z<255&&(m=!0),z>-1){let G=R+","+j+","+W;if(s){let[Q,q,X]=[R,j,W];G=`${[Q,q,X].map(e=>e.toString(16).padStart(2,"0")).join("")}`}fileJSON=(N=N+U+$+G+u)+U+$+G+u,(J+=1)%l==0||D==k-1?(V.push(N),N=""):N+=",",T=-1}}N="";for(let Y=0;Y<V.length;Y++){let K=JSONledStringStart+document.getElementById("brightnessNumber").value+JSONledStringMid1+document.getElementById("targetSegment").value+JSONledStringMid2+V[Y]+"]}}";httpArray.push(K);let ee=curlStart+document.getElementById("curlUrl").value+curlMid1+K+curlEnd;Y>0&&(N+="\n",c+=" && "),N+=K,c+=ee}g="#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"==r?t.value=N:"curl"==r?t.value=c:"ha"==r?t.value=g:t.value="ERROR!/n"+r+" is an unknown format.",fileJSON+="]}}";let et=document.getElementById("image-info"),el=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>"),et.innerHTML=p,el.style.display="block",drawBoxes(E,a,o)}}function drawBoxes(e,t,l){var n=document.getElementById("pixelCanvas"),a=n.getContext("2d");window.innerHeight<window.innerWidth?n.width=Math.floor(.98*window.innerHeight):n.width=Math.floor(.98*window.innerWidth);let o=Math.floor(n.width/t);n.height=o*l+10;for(let r=0;r<l;r++)for(let d=0;d<t;d++){let s=e[r*t+d],_="rgb("+s[0]+", "+s[1]+", "+s[2]+")";s[0],s[1],s[2];let c=s[4];a.fillStyle=_,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(c+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 l=0;l<t;l++){let n=document.createElement("div");n.classList.add("box"),n.style.backgroundColor=getRandomColor(),e.appendChild(n)}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)"}generateSegmentOptions(segmentData),document.getElementById("fileJSONledbutton").innerHTML='<svg style="width:36px;height:36px" viewBox="0 0 24 24"><path fill="currentColor" d="M20 18H4V8H20M20 6H12L10 4H4A2 2 0 0 0 2 6V18A2 2 0 0 0 4 20H20A2 2 0 0 0 22 18V8A2 2 0 0 0 20 6M16 17H14V13H11L15 9L19 13H16Z" /></svg> File to device',document.getElementById("convertbutton").innerHTML='<svg style="width:36px;height:36px" viewBox="0 0 24 24"><path fill="currentColor" d="M12,6V9L16,5L12,1V4A8,8 0 0,0 4,12C4,13.57 4.46,15.03 5.24,16.26L6.7,14.8C6.25,13.97 6,13 6,12A6,6 0 0,1 12,6M18.76,7.74L17.3,9.2C17.74,10.04 18,11 18,12A6,6 0 0,1 12,18V15L8,19L12,23V20A8,8 0 0,0 20,12C20,10.43 19.54,8.97 18.76,7.74Z" /> </svg> Convert to WLED JSON ',document.getElementById("copyJSONledbutton").innerHTML='<svg class="svg-icon" style="width:36px;height:36px" viewBox="0 0 24 24"> <path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z" /> </svg> Copy to clipboard',document.getElementById("sendJSONledbutton").innerHTML='<svg class="svg-icon" style="width:36px;height:36px" viewBox="0 0 24 24"> <path fill="currentColor" d="M6.5 20Q4.22 20 2.61 18.43 1 16.85 1 14.58 1 12.63 2.17 11.1 3.35 9.57 5.25 9.15 5.88 6.85 7.75 5.43 9.63 4 12 4 14.93 4 16.96 6.04 19 8.07 19 11 20.73 11.2 21.86 12.5 23 13.78 23 15.5 23 17.38 21.69 18.69 20.38 20 18.5 20H13Q12.18 20 11.59 19.41 11 18.83 11 18V12.85L9.4 14.4L8 13L12 9L16 13L14.6 14.4L13 12.85V18H18.5Q19.55 18 20.27 17.27 21 16.55 21 15.5 21 14.45 20.27 13.73 19.55 13 18.5 13H17V11Q17 8.93 15.54 7.46 14.08 6 12 6 9.93 6 8.46 7.46 7 8.93 7 11H6.5Q5.05 11 4.03 12.03 3 13.05 3 14.5 3 15.95 4.03 17 5.05 18 6.5 18H9V20M12 13Z" /> </svg> Send to device',window.drawBackground=drawBackground;
</script>
</body>
</html>
================================================
FILE: html/site.webmanifest
================================================
{
"name": "Led Matrix Pixel Art Convertor",
"short_name": "ledconv",
"icons": [
{
"src": "/favicon-32x32.png",
"sizes": "32x322",
"type": "image/png"
},
{
"src": "/favicon-32x32.png",
"sizes": "32x32",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
================================================
FILE: html/statics.js
================================================
var curlStart = 'curl -X POST "http://';
var curlMid1 = '/json/state" -d \'';
var 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 ';
const haMid1 = '\n friendly_name: ';
const haMid2 = '\n unique_id: ';
const haMid3= '\n command_on: >\n ';
const haMid4 = '\n command_off: >\n curl -X POST "http://';
const haEnd = '/json/state" -d \'{"on":false}\' -H "Content-Type: application/json"';
const haCommandLeading = ' ';
const JSONledStringStart = '{"on":true,"bri":';
const JSONledStringMid1 = ',"seg":{"id":';
const JSONledStringMid2 = ',"i":[';
// const JSONledShortStringStart = '{';
// const JSONledShortStringMid1 = '"seg":{"i":[';
const JSONledStringEnd = ']}}';
var accentColor = '#7E4C80';
var accentTextColor = '#777';
var scaleToggleOffd = "M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M7,15A3,3 0 0,1 4,12A3,3 0 0,1 7,9A3,3 0 0,1 10,12A3,3 0 0,1 7,15Z";
var scaleToggleOnd = "M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M17,15A3,3 0 0,1 14,12A3,3 0 0,1 17,9A3,3 0 0,1 20,12A3,3 0 0,1 17,15Z";
================================================
FILE: html/styles.css
================================================
.box {
border: 2px solid white;
}
body {
font-family: 'Arcade', Arial, sans-serif;
background-color: #151515;
}
.top-part {
width: 600px;
margin: 0 auto;
}
.container {
max-width: 100% -40px;
border-radius: 0px;
padding: 20px;
text-align: center;
}
h1 {
font-size: 2.3em;
color: rgb(126, 76, 128);
margin: 1px 0;
font-family: 'Arcade', Arial, sans-serif;
line-height: 0.5;
/*text-align: center;*/
}
h2 {
font-size: 1.1em;
color: rgba(126, 76, 128, 0.61);
margin: 1px 0;
font-family: 'Arcade', Arial, sans-serif;
line-height: 0.5;
text-align: center;
}
h3 {
font-size: 0.7em;
color: rgba(126, 76, 128, 0.61);
margin: 1px 0;
font-family: 'Arcade', Arial, sans-serif;
line-height: 1.4;
text-align: center;
align-items: center;
justify-content: center;
display: flex;
}
p {
font-size: 1.2em;
color: rgb(119, 119, 119);
line-height: 1.5;
font-family: 'Arcade', Arial, sans-serif;
}
#fieldTable {
font-size: 1 em;
color: #777;
line-height: 1;
font-family: 'Arcade', Arial, sans-serif;
}
#scaleTable {
font-size: 1 em;
color: #777;
line-height: 1;
font-family: 'Arcade', Arial, sans-serif;
}
#drop-zone {
display: block;
width: 100%-40px;
border: 3px dashed #7E4C80;
border-radius: 0px;
text-align: center;
padding: 20px;
margin: 0px;
cursor: pointer;
font-family: 'Arcade', Arial, sans-serif;
font-size: 15px;
color: #777;
}
#file-picker {
display: none;
}
* select {
background-color: #333333;
color: #C0C0C0;
border: 1px solid #C0C0C0;
margin-top: 0.5em;
margin-bottom: 0.5em;
padding: 0em;
width: 100%;
height: 27px;
font-size: 15px;
color: rgb(119, 119, 119);
border-radius: 0;
}
* input[type=range] {
-webkit-appearance:none;
flex-grow: 1;
border-radius: 0px;
background: linear-gradient(to right, #333333 0%, #333333 100%);
color: #C0C0C0;
border: 1px solid #C0C0C0;
margin-top: 0.5em;
margin-left: 0em;
}
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;
}
.fullTextField[type=text] {
background-color: #333333;
border: 1px solid #C0C0C0;
padding-inline-start: 5px;
margin-top: 10px;
width: 100%;
height: 24px;
border-radius: 0px;
font-family: 'Arcade', Arial, sans-serif;
font-size: 15px;
color: rgb(119, 119, 119);
}
* input[type=submit] {
background-color: #333333;
border: 1px solid #C0C0C0;
padding: 0.5em;
width: 100%;
border-radius: 0px;
font-family: 'Arcade', Arial, sans-serif;
font-size: 1.3em;
color: rgb(119, 119, 119);
}
* button {
background-color: #333333;
border: 1px solid #C0C0C0;
padding-inline: 5px;
width: 100%;
border-radius: 0px;
font-family: 'Arcade', Arial, sans-serif;
font-size: 1em;
color: rgb(119, 119, 119);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
textarea {
grid-row: 1 / 2;
width: 100%;
height: 200px;
background-color: #333333;
border: 1px solid #C0C0C0;
color: #777;
}
.hide {
display: none;
}
.grids-class{
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;
}
.svg-icon {
vertical-align: middle;
}
.buttondiv-class {
flex: 1;
display: inline-block;
}
.buttondivmid-class {
width: 10px;
display: inline-block;
}
#image-container {
display: grid;
grid-template-rows: 1fr 1fr;
}
#button-container {
display: flex;
padding-bottom: 10px;
padding-top: 10px;
}
.buttonclass {
flex: 1;
padding-top: 5px;
padding-bottom: 5px;
}
.gap {
width: 10px;
}
#submitConvert::before {
content: "";
display: inline-block;
background-image: url('data:image/svg+xml;utf8, <svg style="width:24px;height:24px" viewBox="0 0 24 24" <path fill="currentColor" d="M12,6V9L16,5L12,1V4A8,8 0 0,0 4,12C4,13.57 4.46,15.03 5.24,16.26L6.7,14.8C6.25,13.97 6,13 6,12A6,6 0 0,1 12,6M18.76,7.74L17.3,9.2C17.74,10.04 18,11 18,12A6,6 0 0,1 12,18V15L8,19L12,23V20A8,8 0 0,0 20,12C20,10.43 19.54,8.97 18.76,7.74Z" /></svg>');
width: 36px;
height: 36px;
}
#sizeDiv * {
display: inline-block;
}
.sizeInputFields{
width: 50px;
background-color: #333333;
border: 1px solid #C0C0C0;
padding-inline-start: 5px;
margin-top: -5px;
height: 24px;
border-radius: 0px;
font-family: 'Arcade', Arial, sans-serif;
font-size: 15px;
color: rgb(119, 119, 119);
}
a:link {
color: rgba(126, 76, 128, 0.61);
background-color: transparent;
text-decoration: none;
}
a:visited {
color: rgba(126, 76, 128, 0.61);
background-color: transparent;
text-decoration: none;
}
a:hover {
color: rgb(126, 76, 128);
background-color: transparent;
text-decoration: none;
}
a:active {
color: rgba(126, 76, 128, 0.61);
background-color: transparent;
text-decoration: none;
}
================================================
FILE: infoFiles/FlowDesignDocument.md
================================================
***WLED Command Runner***
**Design document**
Naming conventions
A flow is the “top level” object. It is the container of all other info and is the “thing” we call to start running a command flow.
A step is the “timetable” that the command runner goes through, from top to bottom deciding what sets to call, in what order and how long to wait until running the next set.
A set is what the flow internally handles. When the runner goes through the flow it runs one set and then waits until it is time to run the next set. A set is a (any) number of API commands that are to be run in sequence (without delaying in-between)
A command is any WLED API command, following the standard.
The reason for arranging commands in sets is that WLED has a problem with running to massive commands, i.e. setting hundreds of leds. So the desired outcome needs to be split into several commands that can be run one after another.
flows are uploaded as files (to /edit) with the template flow_[someID].JSON
Usage:
A Flow is made up off steps. Each step references a set (of commands)
A flow runner should, when triggerd,
1. find the file with the correct flow
2. get the array of steps
3. for each step find the set (of commands)
4. Send the commands to WLED
5. Sleep/Wait for the stipulated number of ms
6. Move to next step (as per 4 above)
7. repeat until end of steps
8. If repeat = true start over from step 1
================================================
FILE: infoFiles/runnerFlowTemplate.json
================================================
{
"description" : "My first flow",
"created": "2023-04-05 14:42",
"repeat": true,
"sets": [
{
"id": "numeroUno",
"commands": [
{
"position": 1,
"command": "{Valid WLED API COMMAND}"
},
{
"position": 2,
"command": "{Valid WLED API COMMAND}"
},
{
"position": 3,
"command": "{Valid WLED API COMMAND}"
}
]
},{
"id": "SecondToNone",
"commands": [
{
"position": 1,
"command": "{Valid WLED API COMMAND}"
},
{
"position": 2,
"command": "{Valid WLED API COMMAND}"
},
{
"position": 3,
"command": "{Valid WLED API COMMAND}"
}
]
},{
"id": "4",
"commands": [
{
"position": 1,
"command": "{Valid WLED API COMMAND}"
}
]
},{
"id": "2",
"commands": [
{
"position": 1,
"command": "{Valid WLED API COMMAND}"
},
{
"position": 2,
"command": "{Valid WLED API COMMAND}"
}
]
}
],
"steps":[
{
"step": 1,
"commandId": "SecondToNone",
"duration": 500
},
{
"step": 2,
"commandId": "numeroUno",
"duration": 300
},
{
"step": 3,
"commandId": "SecondToNone",
"duration": 800
},
{
"step": 4,
"commandId": "4",
"duration": 1000
},
{
"step": 5,
"commandId": "2",
"duration": 500
},
{
"step": 6,
"commandId": "SecondToNone",
"duration": 500
}
]
}
================================================
FILE: pixart.htm
================================================
<!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>
WLED Pixel Art Converter</title> <style>
.box{border:2px solid #fff}body{font-family:Arial,sans-serif;background-color:#111}.top-part{width:600px;margin:0 auto}.container{max-width:100% -40px;border-radius:0;padding:20px;text-align:center}h1{font-size:2.3em;color:#ddd;margin:1px 0;font-family:Arial,sans-serif;line-height:.5}h2{font-size:1.1em;color:rgba(221,221,221,.61);margin:1px 0;font-family:Arial,sans-serif;line-height:.5;text-align:center}h3{font-size:.7em;color:rgba(221,221,221,.61);margin:1px 0;font-family:Arial,sans-serif;line-height:1.4;text-align:center;align-items:center;justify-content:center;display:flex}p{font-size:1em;color:#777;line-height:1.5;font-family:Arial,sans-serif}#fieldTable{font-size:1 em;color:#777;line-height:1;font-family:Arial,sans-serif}#scaleTable{font-size:1 em;color:#777;line-height:1;font-family:Arial,sans-serif}#drop-zone{display:block;width:100%-40px;border:3px dashed #ddd;border-radius:0;text-align:center;padding:20px;margin:0;cursor:pointer;font-family:Arial,sans-serif;font-size:15px;color:#777}#file-picker{display:none}.adaptiveTD{display:flex;flex-direction:row;flex-wrap:nowrap;align-items:center}.mainSelector{background-color:#222;color:#ddd;border:1px solid #333;margin-top:4px;margin-bottom:4px;padding:0 8px;height:28px;font-size:15px;border-radius:7px;flex-grow:1;display:flex;align-items:center;justify-content:center}.adaptiveSelector{background-color:#222;color:#ddd;border:1px solid #333;margin-top:4px;margin-bottom:4px;padding:0 8px;height:28px;font-size:15px;border-radius:7px;flex-grow:1;display:none}.segmentsDiv{width:36px;padding-left:5px}* input[type=range]{appearance:none;-moz-appearance:none;-webkit-appearance:none;flex-grow:1;padding:0;margin:4px 8px 4px 0;background-color:transparent;cursor:pointer;background:linear-gradient(to right,#bbb 50%,#333 50%);border-radius:7px}input[type=range]:focus{outline:0}input[type=range]::-webkit-slider-runnable-track{height:28px;cursor:pointer;background:0 0;border-radius:7px}input[type=range]::-webkit-slider-thumb{height:16px;width:16px;border-radius:50%;background:#fff;cursor:pointer;-webkit-appearance:none;margin-top:4px;border-radius:7px}input[type=range]::-moz-range-track{height:28px;background-color:rgba(0,0,0,0);border-radius:7px}input[type=range]::-moz-range-thumb{border:0 solid transparent;height:16px;width:16px;border-radius:7px;background:#fff}.rangeNumber{width:20px;vertical-align:middle}.fullTextField[type=text]{background-color:#222;border:1px solid #333;padding-inline-start:5px;margin-top:4px;margin-bottom:4px;height:24px;border-radius:0;font-family:Arial,sans-serif;font-size:15px;color:#ddd;border-radius:7px;flex-grow:1;display:flex;align-items:center;justify-content:center}.flxTFld{background-color:#222;border:1px solid #333;padding-inline-start:5px;height:24px;border-radius:0;font-family:Arial,sans-serif;font-size:15px;color:#ddd;border-radius:7px;flex-grow:1;display:flex;align-items:center;justify-content:center}* input[type=submit]{background-color:#222;border:1px solid #333;padding:.5em;width:100%;border-radius:24px;font-family:Arial,sans-serif;font-size:1.3em;color:#ddd}* button{background-color:#222;border:1px solid #333;padding-inline:5px;width:100%;border-radius:24px;font-family:Arial,sans-serif;font-size:1em;color:#ddd;display:flex;align-items:center;justify-content:center;cursor:pointer}#scaleDiv{display:flex;align-items:center;vertical-align:middle}textarea{grid-row:1/2;width:100%;height:200px;background-color:#222;border:1px solid #333;color:#ddd}.hide{display:none}.svg-icon{vertical-align:middle}#image-container{display:grid;grid-template-rows:1fr 1fr}#button-container{display:flex;padding-bottom:10px;padding-top:10px}.buttonclass{flex:1;padding-top:5px;padding-bottom:5px}.gap{width:10px}#submitConvert::before{content:"";display:inline-block;background-image:url('data:image/svg+xml;utf8, <svg style="width:24px;height:24px" viewBox="0 0 24 24" <path fill="currentColor" d="M12,6V9L16,5L12,1V4A8,8 0 0,0 4,12C4,13.57 4.46,15.03 5.24,16.26L6.7,14.8C6.25,13.97 6,13 6,12A6,6 0 0,1 12,6M18.76,7.74L17.3,9.2C17.74,10.04 18,11 18,12A6,6 0 0,1 12,18V15L8,19L12,23V20A8,8 0 0,0 20,12C20,10.43 19.54,8.97 18.76,7.74Z" /></svg>');width:36px;height:36px}#sizeDiv *{display:inline-block}.sizeInputFields{width:50px;background-color:#222;border:1px solid #333;padding-inline-start:5px;margin-top:-5px;height:24px;border-radius:7px;font-family:Arial,sans-serif;font-size:15px;color:#ddd}a:link{color:rgba(221,221,221,.61);background-color:transparent;text-decoration:none}a:visited{color:rgba(221,221,221,.61);background-color:transparent;text-decoration:none}a:hover{color:#ddd;background-color:transparent;text-decoration:none}a:active{color:rgba(221,221,221,.61);background-color:transparent;text-decoration:none}
</style> <link rel="shortcut icon"
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAkNJREFUOE+lUstO21AUnHNsF0V+0EXVivAXfAe7qlL7EZEcQ5cQ8nCICbDJF7Trvhas6YbyDdBu2hSk7kihscK1fU91HRKKaFf1xhrP8ejMnCH850P/+v/cC2qcqw4gsGD3Hk/G23+bvSdw7nk1K887BeCQ4AGMAiEjcGaB7gndEfhRqdRQSC9ncQikAVisAWGBFggR5QvQ7UcTNd9mLvDdC2p2rhIN2G4U/vRbneyynzhps70oAPlhNPLjZna1v4/xVmtQHU8tzQXO3CCk4nrPD8MLN+6qd8ef8erwBKvH750XK1Xy4li9/fQFrw9PAGSDg8azuwJDN6jbuep7a+GFF3ezN0en5fDq8Qfn+UqVgjhW5bePpwCKwUHj6a2ACY7zrCdCC34UjrzutjLZiQC/kh0HlyPyu9tKzM4iGPf7uG5sDEwWpYWvrh85RZYAYAIp7+XayNtqytXeLqWbjUUNcLC+fuG2mpLu7tPl5sZDAnpLk0mzFDir+BFJlkCECaKFuSBYSlA4pLWtiRjEygIyErFzyIKjreYTlbZKgaHrRlzoHotweXeAhEhIxPBTDBhQYgEYhNbybINhxY1sbSwwkbHOZrzs3VRmjmnOk6bOkrqxMHT9yCqyxNybAFMg1mWRBGYlg2eboOSFGdT+IwM3Eil6zBwXZF1xkSWkOa6qtHleceuQvC+g4pZXiaOt9jyDb24Q2qKC5XTSvrlK3ZHMW04nLdMPKtQOMzoGTzPz69YNP2vi7D11ftvQWYCzyt7jfwO10TQgL5hT6QAAAABJRU5ErkJggg==">
<script type="text/javascript">
function gId(e){return d.getElementById(e)}function cE(e){return d.createElement(e)}var d=document
</script> </head> <body> <body> <div class="top-part"> <div
style="display:flex;justify-content:center"> <h1
style="display:flex;align-items:center"> <svg
style="width:36px;height:36px;margin-right:6px" viewBox="0 0 32 32"><path
fill="#003fff" d="M6 22h8v4H6zM14 14h4v8h-4zM18 10h4v8h-4zM22 6h8v4h-8z"/></svg>
WLED Pixel Art Converter </h1> </div> <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 class="adaptiveTD"> <select id="ledSetupSelector"
class="mainSelector"> <option value="matrix" selected="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 class="adaptiveTD">
<select id="formatSelector" class="mainSelector"> <option value="wled"
selected="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 class="adaptiveTD"> <select
id="colorFormatSelector" class="mainSelector"> <option value="hex"
selected="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 class="adaptiveTD"> <select id="addressingSelector"
class="mainSelector"> <option value="hybrid" selected="selected">
Hybrid ("f0f0f0",10, 17, "f4f4f4")</option> <option
value="range">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="128"> <span
id="brightnessValue">128</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
class="adaptiveTD"> <input class="fullTextField" 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 class="adaptiveTD"> <input class="fullTextField" 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 class="adaptiveTD"> <input class="fullTextField" 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 class="adaptiveTD"> <input class="fullTextField" type="text"
id="curlUrl" value> </td> </tr> <tr> <td style="vertical-align:middle"> <label
for="targetSegment">Target segment id:</label> </td> <td class="adaptiveTD">
<input class="flxTFld" type="number" id="segID" value="0" min="0" max="63">
<select id="targetSegment" class="adaptiveSelector"> </select> <div
id="getSegmentsDiv" class="segmentsDiv"></div> </td> </tr> </table> <table
class="scaleTableClass" id="scaleTable"
style="width:100%;table-layout:fixed;align-content:center"> <tr> <td
style="vertical-align:middle"> <div id="scaleDiv"> <svg
style="width:36px;height:36px" viewBox="0 0 24 24" onclick="switchScale()"
cursor="pointer"><path fill="currentColor"
d="M17 7H7a5 5 0 0 0-5 5 5 5 0 0 0 5 5h10a5 5 0 0 0 5-5 5 5 0 0 0-5-5M7 15a3 3 0 0 1-3-3 3 3 0 0 1 3-3 3 3 0 0 1 3 3 3 3 0 0 1-3 3z"/>
</svg> Scale image </div> </td> <td style="vertical-align:middle"> <div
id="sizeDiv" style="display:none"> <label for="sizeX">W : </label> <input
class="sizeInputFields" type="number" id="sizeX" min="1" value="16">
<label for="sizeY">H : </label> <input
class="sizeInputFields" type="number" id="sizeY" min="1" value="16"> </div>
</td> </tr> </table> </p> <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"> </p><div
style="width:100%;text-align:center"> <img id="preview"
style="display:none;margin:0 auto" src="data:text/html;base64,"> </div> <div
id="raw-image-container" style="display:none"> <img id="image"
src="data:text/html;base64," alt="RawImage image"> </div> <p></p> <div
id="image-container" style="display:none"> <div id="image-info"
style="display:none"></div> <textarea id="JSONled" readonly="readonly">
</textarea> </div> <div id="button-container" style="display:none"> <button
id="copyJSONledbutton" class="buttonclass"></button> <div id="gap1" class="gap">
</div> <button id="sendJSONledbutton" class="buttonclass"></button> </div> <div>
<h3><div id="version">Version 1.0.8</div> - <a
href="https://github.com/werkstrom/WLED-PixelArtConverter/blob/main/README.md"
target="_blank">Help/About</a></h3> </div> </div> <div id="bottom-part"
style="display:none" class="bottom-part"></div> <canvas id="pixelCanvas">
</canvas> <script type="text/javascript">
for(var gurl=gId("curlUrl"),szX=gId("sizeX"),szY=gId("sizeY"),szDiv=gId("sizeDiv"),prw=gId("preview"),sID=gId("segID"),JLD=gId("JSONled"),tSg=gId("targetSegment"),brgh=gId("brightnessNumber"),seDiv=gId("getSegmentsDiv"),cjb=gId("copyJSONledbutton"),frm=gId("formatSelector"),cLN=gId("colorLimitNumber"),haIDe=gId("haID"),haUe=gId("haUID"),haNe=gId("haName"),aS=gId("addressingSelector"),cFS=gId("colorFormatSelector"),lSS=gId("ledSetupSelector"),imin=gId("image-info"),imcn=gId("image-container"),bcn=gId("button-container"),im=gId("image"),scDiv=gId("scaleDiv"),w=window,canvas=gId("pixelCanvas"),brgV=gId("brightnessValue"),cLV=gId("colorLimitValue"),httpArray=[],fileJSON="",hideableRows=d.querySelectorAll(".ha-hide"),i=0;i<hideableRows.length;i++)hideableRows[i].classList.add("hide");var accentColor="#eee",accentTextColor="#777",prsCol="#ccc",greenColor="#056b0a",redColor="#6b050c",scaleToggleOffd="M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M7,15A3,3 0 0,1 4,12A3,3 0 0,1 7,9A3,3 0 0,1 10,12A3,3 0 0,1 7,15Z",scaleToggleOnd="M17,7H7A5,5 0 0,0 2,12A5,5 0 0,0 7,17H17A5,5 0 0,0 22,12A5,5 0 0,0 17,7M17,15A3,3 0 0,1 14,12A3,3 0 0,1 17,9A3,3 0 0,1 20,12A3,3 0 0,1 17,15Z",sSg=gId("getSegmentsSVGpath")
</script> <script type="text/javascript">
function getPixelRGBValues(e){httpArray=[],fileJSON=`{"on":true,"bri":${brgh.value},"seg":{"id":${tSg.value},"i":[`;let t=0;t="flex"==tSg.style.display?tSg.value:sID.value;const l=parseInt(cLN.value);let n=!1,a=-1;a=frm.selectedIndex;const i=frm.options[a].value;a=lSS.selectedIndex;const o=lSS.options[a].value;a=cFS.selectedIndex;let s=!0;"dec"==cFS.options[a].value&&(s=!1),a=aS.selectedIndex;let r=!0;"single"==aS.options[a].value?r=!1:"hybrid"==aS.options[a].value&&(n=!0);let u="",d="",c='"',h='"';s||(c="[",h="]");let p=!1,f="";var g=cE("canvas"),m=g.getContext("2d",{willReadFrequently:!0}),v=new Image;v.src=e,v.onload=function(){let e=scDiv.children[0].children[0].getAttribute("fill"),a=szX.value,y=szY.value;(e!=accentColor||a<1||y<1)&&(a=v.width,y=v.height,(v.width>512||v.height>512)&&(a=16,y=16)),g.width=a,g.height=y,f="<p>Width: "+a+", Height: "+y+" (make sure this matches your led matrix setup)</p>",m.drawImage(v,0,0,a,y);var S=m.getImageData(0,0,a,y).data,I=[];let $=1;"l2r"==o&&($=0);for(var w=0;w<S.length;w+=4){var b=S[w],x=S[w+1],N=S[w+2],T=S[w+3];let e=w/4,t=Math.floor(e/a),l=e;if("matrix"==o);else if((t+$)%2==0);else{l=t*a+(a-1-(l-t*a))}I.push([b,x,N,T,l,e,t])}I.sort((e,t)=>e[5]-t[5]);let O=[...I];O.sort((e,t)=>e[4]-t[4]);let A="",D=-1,R=O.length,j=0,C=[];for(let e=0;e<R;e++){let t=O[e],a=t[0],i=t[1],o=t[2],u=t[3],d="",f=-1;if(r)if(D<0&&(D=e),e<R-1){let t=O[e+1];t[0]==a&&t[1]==i&&t[2]==o||(f=e+1,d=D==e&&n?""==A?e+",":"":D+","+f+",")}else f=e+1,d=D+1==f&&n?""==A?e+",":"":D+","+f+",";else""==A&&(A=e),D=e,f=e;if(u<255&&(p=!0),f>-1){let t=a+","+i+","+o;if(s){const[e,l,n]=[a,i,o];t=""+[e,l,n].map(e=>e.toString(16).padStart(2,"0")).join("")}A+=d+c+t+h,fileJSON=A+d+c+t+h,j+=1,j%l==0||e==R-1?(C.push(A),A=""):A+=",",D=-1}}A="";for(let e=0;e<C.length;e++){let l=`{"on":true,"bri":${brgh.value},"seg":{"id":${t},"i":[${C[e]}]}}`;httpArray.push(l);let n=`curl -X POST "http://${gurl.value}/json/state" -d '${l}' -H "Content-Type: application/json"`;e>0&&(A+="\n<NEXT COMMAND (multiple commands not supported in API/preset setup)>\n",u+=" && "),A+=l,u+=n}d=`#Uncomment if you don't allready have these defined in your switch section of your configuration.yaml \n#- platform: command_line \n #switches: \n ${haIDe.value} \n friendly_name: ${haNe.value} \n unique_id: ${haUe.value} \n command_on: > \n ${u} \n command_off: > \n curl -X POST "http://${gurl.value}/json/state" -d '{"on":false}' -H "Content-Type: application/json"`,JLD.value="wled"==i?A:"curl"==i?u:"ha"==i?d:"ERROR!/n"+i+" is an unknown format.",fileJSON+="]}}";let _=imin,H=imin;p&&(f+="<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>"),_.innerHTML=f,H.style.display="block",drawBoxes(I,a,y)}}
</script> <script type="text/javascript">
function drawBoxes(t,e,a){var i=window,n=canvas.getContext("2d",{willReadFrequently:!0});i.innerHeight<i.innerWidth?canvas.width=Math.floor(.98*i.innerHeight):canvas.width=Math.floor(.98*i.innerWidth);let l=Math.floor(canvas.width/e),h=(i.innerWidth-e*l)/2;canvas.height=l*a+10;for(let i=0;i<a;i++)for(let a=0;a<e;a++){let h=t[i*e+a],r="rgb("+h[0]+", "+h[1]+", "+h[2]+")",d="rgb(128,128,128)";n.fillStyle=r,n.fillRect(a*l,i*l,l,l),n.strokeStyle="#888888",n.lineWidth=1,n.strokeRect(a*l,i*l,l,l),n.font="10px Arial",n.fillStyle=d,n.textAlign="center",n.textBaseline="middle",n.fillText(h[4]+1,a*l+l/2,i*l+l/2)}var r=n.getImageData(0,0,canvas.width,canvas.height);n.clearRect(0,0,canvas.width,canvas.height),canvas.width=i.innerWidth,n.putImageData(r,h,0)}
</script> <script type="text/javascript">
let devMode=!1;gurl.value=location.host;const urlParams=new URLSearchParams(window.location.search);function gen(){if((szX.value>0&&szY.value>0||"none"==szDiv.style.display)&&gurl.value.length>0&&"none"!=prw.style.display){let e=prw.src;if(isValidBase64Gif(e))im.src=e,getPixelRGBValues(e),imcn.style.display="block",bcn.style.display="";else{let t="<p><b>WARNING!</b> File does not appear to be a valid image</p>";imin.innerHTML=t,imin.style.display="block",imcn.style.display="none",JLD.value="",devMode&&console.log("The string '"+e+"' is not a valid base64 image.")}}if(gurl.value.length>0)gId("sSg").setAttribute("fill",accentColor);else{gId("sSg").setAttribute("fill",accentTextColor);let e=tSg;e.style.display="none",e.innerHTML="",sID.style.display="flex"}}async function postPixels(){let e=gId("sendSvgP");e.setAttribute("fill",prsCol);let t=!1;for(let e of httpArray)try{devMode&&console.log(e),devMode&&console.log(e.length);const t=await fetch("http://"+gId("curlUrl").value+"/json/state",{method:"POST",headers:{"Content-Type":"application/json"},body:e}),n=await t.json();devMode&&console.log(n)}catch(e){console.error(e),t=!0}t?(e.setAttribute("fill",redColor),setTimeout((function(){e.setAttribute("fill",accentTextColor)}),1e3)):(e.setAttribute("fill",greenColor),setTimeout((function(){e.setAttribute("fill",accentColor)}),1e3))}gurl.value.length<1&&(gurl.value="Missing_Host"),cjb.addEventListener("click",async()=>{let e=JLD;e.select();try{await navigator.clipboard.writeText(e.value)}catch(e){try{await d.execCommand("copy")}catch(e){console.error("Failed to copy text: ",e)}}}),lSS.addEventListener("change",gen),szY.addEventListener("change",gen),szX.addEventListener("change",gen),cFS.addEventListener("change",gen),aS.addEventListener("change",gen),brgh.addEventListener("change",gen),cLN.addEventListener("change",gen),haIDe.addEventListener("change",gen),haUe.addEventListener("change",gen),haNe.addEventListener("change",gen),gurl.addEventListener("change",gen),sID.addEventListener("change",gen),prw.addEventListener("load",gen),tSg.addEventListener("change",()=>{sop=tSg.options[tSg.selectedIndex],szX.value=sop.dataset.x,szY.value=sop.dataset.y,gen()}),gId("sendJSONledbutton").addEventListener("click",async()=>{"https:"===window.location.protocol?alert("Will only be available when served over http (or WLED is run over https)"):postPixels()}),brgh.oninput=()=>{brgV.textContent=brgh.value;let e=100*parseInt(brgh.value)/255;var t=`linear-gradient(90deg, #bbb ${e}%, #333 ${e}%)`;brgh.style.backgroundImage=t},cLN.oninput=()=>{let e=cLN;cLV.textContent=e.value;let t=100*parseInt(e.value)/512;var n=`linear-gradient(90deg, #bbb ${t}%, #333 ${t}%)`;e.style.backgroundImage=n},frm.addEventListener("change",()=>{for(var e=0;e<hideableRows.length;e++)hideableRows[e].classList.toggle("hide","ha"!==frm.value),gen()});const dropZone=gId("drop-zone"),filePicker=gId("file-picker"),preview=prw;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");updatePreview(e.dataTransfer.files[0])}function filePicked(e){updatePreview(e.target.files[0])}function updatePreview(e){const t=new FileReader;t.onload=()=>{preview.src=t.result,prw.style.display=""},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);for(var hideableRows=d.querySelectorAll(".ha-hide"),i=0;i<hideableRows.length;i++)hideableRows[i].classList.add("hide");function switchScale(){let e=scDiv.children[0].children[0],t=e.getAttribute("fill"),n="";t===accentColor?(t=accentTextColor,n=scaleToggleOffd,szDiv.style.display="none"):(t=accentColor,n=scaleToggleOnd,szDiv.style.display=""),e.setAttribute("fill",t),e.setAttribute("d",n),gen()}function generateSegmentOptions(e){tSg.innerHTML="";for(var t=0;t<e.length;t++){var n=cE("option");n.value=e[t].value,n.text=e[t].text,n.dataset.x=e[t].x,n.dataset.y=e[t].y,tSg.appendChild(n),0===t&&(n.selected=!0,szX.value=n.dataset.x,szY.value=n.dataset.y)}}async function getSegments(){if(cv=gurl.value,cv.length>0)try{var e=[];const n=await fetch("http://"+cv+"/json/state");let l=(await n.json()).seg.map(e=>({id:e.id,n:e.n,xs:e.start,xe:e.stop,ys:e.startY,ye:e.stopY}));for(var t=0;t<l.le
gitextract_fy85wnci/ ├── .github/ │ └── workflows/ │ ├── env.yml │ └── manual.yml ├── LICENSE ├── README.md ├── RELEASENOTES.md ├── archived/ │ ├── pixart.htm │ └── pixartmin.htm ├── beta/ │ ├── pixart.htm │ └── releasenotes.md ├── examples/ │ └── filename ├── html/ │ ├── boxdraw.js │ ├── getPixelValues.js │ ├── index.html │ ├── index.js │ ├── pixartmin.html │ ├── site.webmanifest │ ├── statics.js │ └── styles.css ├── infoFiles/ │ ├── FlowDesignDocument.md │ └── runnerFlowTemplate.json └── pixart.htm
SYMBOL INDEX (18 symbols across 3 files)
FILE: html/boxdraw.js
function drawBoxes (line 1) | function drawBoxes(inputPixelArray, widthPixels, heightPixels) {
function drawBackground (line 62) | function drawBackground() {
function getRandomColor (line 81) | function getRandomColor() {
FILE: html/getPixelValues.js
function getPixelRGBValues (line 1) | function getPixelRGBValues(base64Image) {
FILE: html/index.js
function postPixels (line 88) | async function postPixels() {
function zoneClicked (line 123) | function zoneClicked(e) {
function dragEnter (line 131) | function dragEnter(e) {
function dragOver (line 137) | function dragOver(e) {
function dropped (line 142) | function dropped(e) {
function filePicked (line 152) | function filePicked(e) {
function updatePreview (line 159) | function updatePreview(file) {
function isValidBase64Gif (line 170) | function isValidBase64Gif(string) {
function switchScale (line 207) | function switchScale() {
function sendAsFile (line 228) | function sendAsFile(jsonStringInput, fileName, urlString) {
function generateSegmentOptions (line 250) | function generateSegmentOptions(array) {
function generateSegmentArray (line 267) | function generateSegmentArray(noOfSegments) {
function changeColorOfDot (line 283) | function changeColorOfDot() {
function randomInterval (line 301) | function randomInterval() {
Condensed preview — 21 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (209K chars).
[
{
"path": ".github/workflows/env.yml",
"chars": 48,
"preview": "version_string: \"Version ${{ inputs.version }}\"\n"
},
{
"path": ".github/workflows/manual.yml",
"chars": 1853,
"preview": "# This is a basic workflow that is manually triggered\n\nname: Manual workflow\n\n# Controls when the action will run. Workf"
},
{
"path": "LICENSE",
"chars": 1073,
"preview": "MIT License\n\nCopyright (c) 2023 Henrik Werkström\n\nPermission is hereby granted, free of charge, to any person obtaining "
},
{
"path": "README.md",
"chars": 9796,
"preview": "# Download instructions\nFor stable (1.0.8) \n***Right***-Click [here](https://raw.githubusercontent.com/werkstrom/WLED-P"
},
{
"path": "RELEASENOTES.md",
"chars": 1698,
"preview": "# Version 1.0.6 \n## Main take aways \n- Streamlined the conversion process \n - Image generates as soon as there is eno"
},
{
"path": "archived/pixart.htm",
"chars": 24539,
"preview": "<!DOCTYPE html> <html> <head> <meta http-equiv=\"Cache-Control\"\ncontent=\"no-cache, no-store, must-revalidate\"> <meta http"
},
{
"path": "archived/pixartmin.htm",
"chars": 26423,
"preview": "<!DOCTYPE html> <html> <head> <meta http-equiv=\"Cache-Control\" \ncontent=\"no-cache, no-store, must-revalidate\"> <meta htt"
},
{
"path": "beta/pixart.htm",
"chars": 38031,
"preview": "<!DOCTYPE html> <html> <head> <meta http-equiv=\"Cache-Control\" \ncontent=\"no-cache, no-store, must-revalidate\"> <meta htt"
},
{
"path": "beta/releasenotes.md",
"chars": 5,
"preview": "Beta\n"
},
{
"path": "examples/filename",
"chars": 1,
"preview": "\n"
},
{
"path": "html/boxdraw.js",
"chars": 2798,
"preview": "function drawBoxes(inputPixelArray, widthPixels, heightPixels) {\n \n // Get a reference to the canvas element\n var "
},
{
"path": "html/getPixelValues.js",
"chars": 10938,
"preview": "function getPixelRGBValues(base64Image) {\n httpArray = [];\n fileJSON = JSONledStringStart + document.getElementById('b"
},
{
"path": "html/index.html",
"chars": 10895,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <meta http-equiv=\"Cache-Control\" content=\"no-cache, no-store, must-revalidate\">\n "
},
{
"path": "html/index.js",
"chars": 11258,
"preview": "//Start up code\ndocument.getElementById('curlUrl').value = location.host;\n\nlet devMode = false;\nconst urlParams = new UR"
},
{
"path": "html/pixartmin.html",
"chars": 26162,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <meta http-equiv=\"Cache-Control\" content=\"no-cache, no-store, must-revalidate\">\n "
},
{
"path": "html/site.webmanifest",
"chars": 426,
"preview": "{\n \"name\": \"Led Matrix Pixel Art Convertor\",\n \"short_name\": \"ledconv\",\n \"icons\": [\n {\n \"src\": \"/"
},
{
"path": "html/statics.js",
"chars": 1269,
"preview": "var curlStart = 'curl -X POST \"http://';\nvar curlMid1 = '/json/state\" -d \\'';\nvar curlEnd = '\\' -H \"Content-Type: applic"
},
{
"path": "html/styles.css",
"chars": 5028,
"preview": "\n.box {\n border: 2px solid white;\n}\nbody {\n font-family: 'Arcade', Arial, sans-serif;\n background-color: #151515;\n}\n\n"
},
{
"path": "infoFiles/FlowDesignDocument.md",
"chars": 1404,
"preview": "***WLED Command Runner*** \n**Design document**\n\nNaming conventions\nA flow is the “top level” object. It is the containe"
},
{
"path": "infoFiles/runnerFlowTemplate.json",
"chars": 2299,
"preview": "{\n \"description\" : \"My first flow\",\n \"created\": \"2023-04-05 14:42\",\n \"repeat\": true,\n \"sets\": [\n {\n "
},
{
"path": "pixart.htm",
"chars": 24239,
"preview": "<!DOCTYPE html> <html> <head> <meta http-equiv=\"Cache-Control\" \ncontent=\"no-cache, no-store, must-revalidate\"> <meta htt"
}
]
About this extraction
This page contains the full source code of the werkstrom/WLED-PixelArtConverter GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 21 files (195.5 KB), approximately 67.8k tokens, and a symbol index with 18 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.