diff --git a/html/boxdraw.js b/html/boxdraw.js new file mode 100644 index 0000000..4da9572 --- /dev/null +++ b/html/boxdraw.js @@ -0,0 +1,100 @@ +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.style.cssText = ` + 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; + `; + + 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; diff --git a/html/favicon-16x16.png b/html/favicon-16x16.png new file mode 100644 index 0000000..feb51ca Binary files /dev/null and b/html/favicon-16x16.png differ diff --git a/html/favicon-32x32.png b/html/favicon-32x32.png new file mode 100644 index 0000000..a3b5cce Binary files /dev/null and b/html/favicon-32x32.png differ diff --git a/html/favicon.ico b/html/favicon.ico new file mode 100644 index 0000000..bde8945 Binary files /dev/null and b/html/favicon.ico differ diff --git a/html/getPixelValues.js b/html/getPixelValues.js new file mode 100644 index 0000000..6ab8b2c --- /dev/null +++ b/html/getPixelValues.js @@ -0,0 +1,253 @@ +function getPixelRGBValues(base64Image) { + httpArray = []; + const copyJSONledbutton = document.getElementById('copyJSONledbutton'); + const JSONled = document.getElementById('JSONled'); + const maxNoOfColorsInCommandSting = document.getElementById('colorLimitNumber').value; + + 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 (selector.options[selectedIndex].value == 'single'){ + segmentValueCheck = false + } + + 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() { + // Set the canvas size to the same as the image + canvas.width = image.width; + canvas.height = image.height; + imageInfo = '

Width: ' + image.width + ', Height: ' + image.height + ' (make sure this matches your led matrix setup)

' + + // Draw the image onto the canvas + context.drawImage(image, 0, 0); + + // Get the pixel data from the canvas + var pixelData = context.getImageData(0, 0, image.width, image.height).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/image.width); + 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 * image.width); + let maxIndexOnRow = image.width - 1; + let reversedIndexOnRow = maxIndexOnRow - indexOnRow; + led = (row * image.width) + 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... + segmentString = segmentStart + ',' + segmentEnd + ','; + } + + } else { + //This is the last pixel, so the segment must end + segmentEnd = i + 1; + segmentString = segmentStart + ',' + segmentEnd + ','; + } + } else{ + //Write every pixel + if (JSONledString == ''){ + //If addressing is single, we ned to start every command with a starting possition + JSONledString = i + ', \'dummy\','; + } + + 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 + } + + JSONledString = 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 + 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.' + } + + let infoDiv = document.getElementById('image-info'); + let canvasDiv = document.getElementById('image-info'); + if (hasTransparency){ + imageInfo = imageInfo + '

WARNING! 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.

' + } + + infoDiv.innerHTML = imageInfo; + canvasDiv.style.display = "block" + + //Drawing the image + drawBoxes(pixelRGBValues, image.width, image.width); + } +} \ No newline at end of file diff --git a/html/index.html b/html/index.html new file mode 100644 index 0000000..5ed09ca --- /dev/null +++ b/html/index.html @@ -0,0 +1,236 @@ + + + + + + + + Led Matrix Pixel Art Convertor + + + + + + +
+

Led Matrix Pixel Art Converter

+

Convert image to WLED JSON (pixel art on WLED matrix)

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + + 100 +
+ + + + 256 +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + + +
+ + +

+

+ + + +

+

+ Drop image here
or
+ Click to select a file +
+ +

+ +

+ +

+
+
+ +
+ +
+ + +

+ + +
+ + + + + + + + + \ No newline at end of file diff --git a/html/index.js b/html/index.js new file mode 100644 index 0000000..b7b6004 --- /dev/null +++ b/html/index.js @@ -0,0 +1,179 @@ +//Start up code +console.log(location.host); +document.getElementById('curlUrl').value = location.host; +let httpArray = []; + + +//On submit button pressed ======================= +document.getElementById('form').addEventListener('submit', function(event) { + + event.preventDefault(); + + let base64Image = document.getElementById('preview').src; + if (isValidBase64Gif(base64Image)) { + document.getElementById('image').src = base64Image; + getPixelRGBValues(base64Image); + document.getElementById('image-container').style.display = "block" + } + else { + let infoDiv = document.getElementById('image-info'); + let imageInfo = '

WARNING! File does not appear to be a valid GIF image

' + 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 GIF image."); + } + +}); + +// Code for copying the generated string to clipboard + +copyJSONledbutton.addEventListener('click', async () => { + JSONled.select(); + try { + await navigator.clipboard.writeText('test text'); + console.log('Text copied to clipboard'); + } 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(); + } +}); + +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); + } + } +} + +let helpCheckbox = document.getElementById("helpCheckbox"); +let helpDiv = document.getElementById("help-container"); + +helpCheckbox.addEventListener("change", function() { + if (helpCheckbox.checked) { + helpDiv.style.display = "block"; + } else { + helpDiv.style.display = "none"; + } +}); + +//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("submitConvert").style.display = "block"; + }; + 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. Leavingg 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"); + } + }); \ No newline at end of file diff --git a/html/site.webmanifest b/html/site.webmanifest new file mode 100644 index 0000000..3072091 --- /dev/null +++ b/html/site.webmanifest @@ -0,0 +1,19 @@ +{ + "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" + } \ No newline at end of file diff --git a/html/statics.js b/html/statics.js new file mode 100644 index 0000000..07ff027 --- /dev/null +++ b/html/statics.js @@ -0,0 +1,18 @@ +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":{"i":['; +const JSONledShortStringStart = '{'; +const JSONledShortStringMid1 = '"seg":{"i":['; +const JSONledStringEnd = ']}}'; + diff --git a/html/styles.css b/html/styles.css new file mode 100644 index 0000000..4c41037 --- /dev/null +++ b/html/styles.css @@ -0,0 +1,166 @@ + + .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: 20px 0; + font-family: 'Arcade', Arial, sans-serif; + line-height: 0.5; + text-align: center; + } + h2 { + font-size: 1.3em; + color: rgba(126, 76, 128, 0.61); + margin: 20px 0; + font-family: 'Arcade', Arial, sans-serif; + line-height: 0.5; + text-align: center; + } + + 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; + } + + #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; + } + + + *.button { + display: block; + width: 100% - 40px; + border: 2px dashed #ccc; + border-radius: 20px; + text-align: center; + padding: 20px; + margin: 0px; + cursor: pointer; + } + + #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; + } + * input[type=text] { + background-color: #333333; + border: 1px solid #C0C0C0; + padding-inline-start: 5px; + margin-top: 10px; + width: 100%; + height: 27px; + border-radius: 0px; + font-family: 'Arcade', Arial, sans-serif; + font-size: 15px; + color: rgb(119, 119, 119); + } + * input[type="checkbox"] { + } + * 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: 0.5em; + margin-bottom: 15px; + width: 100%; + border-radius: 0px; + font-family: 'Arcade', Arial, sans-serif; + font-size: 1.3em; + color: rgb(119, 119, 119); + } + * textarea { + background-color: #333333; + border: 1px solid #C0C0C0; + padding: 0em; + margin-bottom: 10px; + width: 100%; + height: 200px; + border-radius: 0px; + font-family: 'Courier', Arial, sans-serif; + font-size: 1em; + color: rgb(119, 119, 119); + } + .hide { + display: none; + } \ No newline at end of file