Create a small writer method to reuse with all conversion types instead of handling formatting separate for each type

This commit is contained in:
javl
2026-08-15 09:17:42 +02:00
parent 43e3af1b81
commit 3da6cf9607
+139 -202
View File
@@ -36,54 +36,65 @@ function bitswap(b) {
return b; return b;
} }
// Accumulates formatted bytes (prefix/value/separator, wrapped every
// wrapWidth bytes) so our ConversionFunctions can use this formatting.
// wrapWidth defaults to 16 bytes per line; horizontal888 wraps
// once per image row (canvasWidth) instead.
function makeByteWriter(wrapWidth = 16) {
let out = '';
let count = 0;
return {
push(value, hexWidth) {
const byteSet = bitswap(value).toString(16).padStart(hexWidth, '0');
out += `${settings.prefix}${byteSet}${settings.separator}`;
count++;
if (count >= wrapWidth) {
out += '\n';
count = 0;
}
},
result() { return out; },
};
}
// Shared by horizontal1bit/horizontalAlpha: pack 8 samples (one per pixel,
// MSB first) into a byte, resetting early at the end of a row or the image
// so a partial final byte is still zero-padded instead of dropped.
function packBitsHorizontal(data, canvasWidth, writer, sampleAt) {
let byteIndex = 7;
let number = 0;
for (let index = 0; index < data.length; index += 4) {
if (sampleAt(data, index) > settings.ditheringThreshold) {
number += 2 ** byteIndex;
}
byteIndex--;
const isRowEnd = index !== 0 && (((index / 4) + 1) % canvasWidth) === 0;
const isImageEnd = index === data.length - 4;
if (isRowEnd || isImageEnd) {
byteIndex = -1;
}
if (byteIndex < 0) {
writer.push(number, 2);
number = 0;
byteIndex = 7;
}
}
}
const ConversionFunctions = { const ConversionFunctions = {
// Output the image as a string for horizontally drawing displays // Output the image as a string for horizontally drawing displays
horizontal1bit(data, canvasWidth) { horizontal1bit(data, canvasWidth) {
let stringFromBytes = ''; const writer = makeByteWriter();
let outputIndex = 0; const avgRgb = (d, i) => (d[i] + d[i + 1] + d[i + 2]) / 3;
let byteIndex = 7; packBitsHorizontal(data, canvasWidth, writer, avgRgb);
let number = 0; return writer.result();
// format is RGBA, so move 4 steps per pixel
for (let index = 0; index < data.length; index += 4) {
// Get the average of the RGB (we ignore A)
const avg = (data[index] + data[index + 1] + data[index + 2]) / 3;
if (avg > settings.ditheringThreshold) {
number += 2 ** byteIndex;
}
byteIndex--;
// if this was the last pixel of a row or the last pixel of the
// image, fill up the rest of our byte with zeros so it always contains 8 bits
if ((index !== 0 && (((index / 4) + 1) % (canvasWidth)) === 0) || (index === data.length - 4)) {
// for(var i=byteIndex;i>-1;i--){
// number += Math.pow(2, i);
// }
byteIndex = -1;
}
// When we have the complete 8 bits, combine them into a hex value
if (byteIndex < 0) {
let byteSet = bitswap(number).toString(16);
if (byteSet.length === 1) { byteSet = `0${byteSet}`; }
stringFromBytes += `${settings.prefix}${byteSet}${settings.separator}`;
outputIndex++;
if (outputIndex >= 16) {
stringFromBytes += '\n';
outputIndex = 0;
}
number = 0;
byteIndex = 7;
}
}
return stringFromBytes;
}, },
// Output the image as a string for vertically drawing displays // Output the image as a string for vertically drawing displays
// eslint-disable-next-line no-unused-vars vertical1bit(data) {
vertical1bit(data, canvasWidth) { const writer = makeByteWriter();
let stringFromBytes = '';
let outputIndex = 0;
for (let p = 0; p < Math.ceil(settings.screenHeight / 8); p++) { for (let p = 0; p < Math.ceil(settings.screenHeight / 8); p++) {
for (let x = 0; x < settings.screenWidth; x++) { for (let x = 0; x < settings.screenWidth; x++) {
let byteIndex = 7; let byteIndex = 7;
@@ -97,118 +108,49 @@ const ConversionFunctions = {
} }
byteIndex--; byteIndex--;
} }
let byteSet = bitswap(number).toString(16); writer.push(number, 2);
if (byteSet.length === 1) { byteSet = `0${byteSet}`; }
stringFromBytes += `${settings.prefix}${byteSet}${settings.separator}`;
outputIndex++;
if (outputIndex >= 16) {
stringFromBytes += '\n';
outputIndex = 0;
}
} }
} }
return stringFromBytes; return writer.result();
}, },
// Output the image as a string for 565 displays (horizontally) // Output the image as a string for 565 displays (horizontally)
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
horizontal565(data, canvasWidth) { horizontal565(data, canvasWidth) {
let stringFromBytes = ''; const writer = makeByteWriter();
let outputIndex = 0;
// format is RGBA, so move 4 steps per pixel // format is RGBA, so move 4 steps per pixel
for (let index = 0; index < data.length; index += 4) { for (let index = 0; index < data.length; index += 4) {
// Get the RGB values
const r = data[index]; const r = data[index];
const g = data[index + 1]; const g = data[index + 1];
const b = data[index + 2]; const b = data[index + 2];
// calculate the 565 color value // Calculate the 565 color value
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
const rgb = ((r & 0b11111000) << 8) | ((g & 0b11111100) << 3) | ((b & 0b11111000) >> 3); const rgb = ((r & 0b11111000) << 8) | ((g & 0b11111100) << 3) | ((b & 0b11111000) >> 3);
// Split up the color value in two bytes writer.push(rgb, 4);
// const firstByte = (rgb >> 8) & 0xff;
// const secondByte = rgb & 0xff;
let byteSet = bitswap(rgb).toString(16);
while (byteSet.length < 4) { byteSet = `0${byteSet}`; }
stringFromBytes += `${settings.prefix}${byteSet}${settings.separator}`;
// add newlines every 16 bytes
outputIndex++;
if (outputIndex >= 16) {
stringFromBytes += '\n';
outputIndex = 0;
}
} }
return stringFromBytes; return writer.result();
}, },
// Output the image as a string for rgb888 displays (horizontally) // Output the image as a string for rgb888 displays (horizontally), one
// image row per output line
horizontal888(data, canvasWidth) { horizontal888(data, canvasWidth) {
let stringFromBytes = ''; const writer = makeByteWriter(canvasWidth);
let outputIndex = 0;
// format is RGBA, so move 4 steps per pixel // format is RGBA, so move 4 steps per pixel
for (let index = 0; index < data.length; index += 4) { for (let index = 0; index < data.length; index += 4) {
// Get the RGB values // Split into RGB and pack into a single 24-bit value (MSB first)
const r = data[index]; const r = data[index];
const g = data[index + 1]; const g = data[index + 1];
const b = data[index + 2]; const b = data[index + 2];
// calculate the 565 color value
// eslint-disable-next-line no-bitwise // eslint-disable-next-line no-bitwise
const rgb = (r << 16) | (g << 8) | (b); const rgb = (r << 16) | (g << 8) | (b);
// Split up the color value in two bytes writer.push(rgb, 8);
// const firstByte = (rgb >> 8) & 0xff;
// const secondByte = rgb & 0xff;
let byteSet = bitswap(rgb).toString(16);
while (byteSet.length < 8) { byteSet = `0${byteSet}`; }
stringFromBytes += `${settings.prefix}${byteSet}${settings.separator}`;
// add newlines every 16 bytes
outputIndex++;
if (outputIndex >= canvasWidth) {
stringFromBytes += '\n';
outputIndex = 0;
}
} }
return stringFromBytes; return writer.result();
}, },
// Output the alpha mask as a string for horizontally drawing displays // Output the alpha mask as a string for horizontally drawing displays
horizontalAlpha(data, canvasWidth) { horizontalAlpha(data, canvasWidth) {
let stringFromBytes = ''; const writer = makeByteWriter();
let outputIndex = 0; packBitsHorizontal(data, canvasWidth, writer, (d, i) => d[i + 3]);
let byteIndex = 7; return writer.result();
let number = 0;
// format is RGBA, so move 4 steps per pixel
for (let index = 0; index < data.length; index += 4) {
// Get alpha part of the image data
const alpha = data[index + 3];
if (alpha > settings.ditheringThreshold) {
number += 2 ** byteIndex;
}
byteIndex--;
// if this was the last pixel of a row or the last pixel of the
// image, fill up the rest of our byte with zeros so it always contains 8 bits
if ((index !== 0 && (((index / 4) + 1) % (canvasWidth)) === 0) || (index === data.length - 4)) {
byteIndex = -1;
}
// When we have the complete 8 bits, combine them into a hex value
if (byteIndex < 0) {
let byteSet = bitswap(number).toString(16);
if (byteSet.length === 1) { byteSet = `0${byteSet}`; }
stringFromBytes += `${settings.prefix}${byteSet}${settings.separator}`;
outputIndex++;
if (outputIndex >= 16) {
stringFromBytes += '\n';
outputIndex = 0;
}
number = 0;
byteIndex = 7;
}
}
return stringFromBytes;
}, },
}; };
settings.conversionFunction = ConversionFunctions.horizontal1bit; settings.conversionFunction = ConversionFunctions.horizontal1bit;
@@ -489,10 +431,50 @@ function getImageType() {
return 'unsigned char'; return 'unsigned char';
} }
// Use the horizontally oriented list to draw the image
// Validates and decodes a comma-split list of pasted hex byte strings into
// one contiguous bit string (MSB first, each entry padded to a full byte).
// Returns { valid: true, bits } or { valid: false, s } naming the bad entry.
function hexListToBits(list) {
let bits = '';
for (let i = 0; i < list.length; i++) {
const binString = hexToBinary(list[i]);
if (!binString.valid) {
return binString;
}
bits += binString.result.length === 4 ? `${binString.result}0000` : binString.result;
}
return { valid: true, bits };
}
function reportInvalidByteArray(s) {
// eslint-disable-next-line no-alert
alert('Something went wrong converting the string. Make sure there are no comments in your input.');
// eslint-disable-next-line no-console
console.error('invalid hexToBinary: ', s);
}
// Save the canvas contents inside the (first) image object so it can be
// reused when scaling/inverting/etc.
function commitCanvasToFirstImage(canvas) {
const img = new Image();
img.onload = () => {
images.first().img = img;
};
img.src = canvas.toDataURL('image/png');
}
// Use the horizontally oriented list to draw the image // Use the horizontally oriented list to draw the image
function listToImageHorizontal(list, canvas) { function listToImageHorizontal(list, canvas) {
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
const decoded = hexListToBits(list);
if (!decoded.valid) {
reportInvalidByteArray(decoded.s);
return;
}
const imgData = ctx.createImageData(canvas.width, canvas.height); const imgData = ctx.createImageData(canvas.width, canvas.height);
let index = 0; let index = 0;
@@ -501,51 +483,24 @@ function listToImageHorizontal(list, canvas) {
let widthCounter = 0; let widthCounter = 0;
// Move the list into the imageData object // Move the list into the imageData object
for (let i = 0; i < list.length; i++) { for (let k = 0; k < decoded.bits.length; k++, widthCounter++) {
let binString = hexToBinary(list[i]); // if we've counted enough bits, reset counter for next line
if (!binString.valid) { if (widthCounter >= widthRoundedUp) {
// eslint-disable-next-line no-alert widthCounter = 0;
alert('Something went wrong converting the string. Make sure there are no comments in your input?');
// eslint-disable-next-line no-console
console.error('invalid hexToBinary: ', binString.s);
return;
} }
binString = binString.result; // skip 'artifact' pixels due to rounding up to a byte
if (binString.length === 4) { if (widthCounter < canvas.width) {
binString += '0000'; const color = decoded.bits.charAt(k) === '1' ? 255 : 0;
} imgData.data[index] = color;
imgData.data[index + 1] = color;
// Check if pixel is white or black imgData.data[index + 2] = color;
for (let k = 0; k < binString.length; k++, widthCounter++) { imgData.data[index + 3] = 255;
// if we've counted enough bits, reset counter for next line index += 4;
if (widthCounter >= widthRoundedUp) {
widthCounter = 0;
}
// skip 'artifact' pixels due to rounding up to a byte
if (widthCounter < canvas.width) {
let color = 0;
if (binString.charAt(k) === '1') {
color = 255;
}
imgData.data[index] = color;
imgData.data[index + 1] = color;
imgData.data[index + 2] = color;
imgData.data[index + 3] = 255;
index += 4;
}
} }
} }
// Draw the image onto the canvas, then save the canvas contents
// inside the img object. This way we can reuse the img object when
// we want to scale / invert, etc.
ctx.putImageData(imgData, 0, 0); ctx.putImageData(imgData, 0, 0);
const img = new Image(); commitCanvasToFirstImage(canvas);
img.onload = () => {
images.first().img = img;
};
img.src = canvas.toDataURL('image/png');
} }
// Quick and effective way to draw single pixels onto the canvas // Quick and effective way to draw single pixels onto the canvas
@@ -566,50 +521,32 @@ function listToImageVertical(list, canvas) {
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
const decoded = hexListToBits(list);
if (!decoded.valid) {
reportInvalidByteArray(decoded.s);
return;
}
let page = 0; let page = 0;
let x = 0; let x = 0;
let y = 7; let y = 7;
// Move the list into the imageData object // Move the list into the imageData object
for (let i = 0; i < list.length; i++) { for (let k = 0; k < decoded.bits.length; k++) {
let binString = hexToBinary(list[i]); const color = decoded.bits.charAt(k) === '1' ? 255 : 0;
if (!binString.valid) { drawPixel(ctx, x, (page * 8) + y, color);
// eslint-disable-next-line no-alert y--;
alert('Something went wrong converting the string. Did you forget to remove any comments from the input?'); if (y < 0) {
// eslint-disable-next-line no-console y = 7;
console.error('invalid hexToBinary: ', binString.s); x++;
return; if (x >= settings.screenWidth) {
} x = 0;
binString = binString.result; page++;
if (binString.length === 4) {
binString += '0000';
}
// Check if pixel is white or black
for (let k = 0; k < binString.length; k++) {
let color = 0;
if (binString.charAt(k) === '1') {
color = 255;
}
drawPixel(ctx, x, (page * 8) + y, color);
y--;
if (y < 0) {
y = 7;
x++;
if (x >= settings.screenWidth) {
x = 0;
page++;
}
} }
} }
} }
// Save the canvas contents inside the img object. This way we can
// reuse the img object when we want to scale / invert, etc. commitCanvasToFirstImage(canvas);
const img = new Image();
img.onload = () => {
images.first().img = img;
};
img.src = canvas.toDataURL('image/png');
} }
// Set width/height preset for byte array text input // Set width/height preset for byte array text input