You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, paletteString = "#000000,#FFFFFF", algorithm = "FloydSteinberg", thresholdVal = 128, bayerMatrixSize = 4, bayerLevels = 2) {
// Helper function: Clamp value to a range
function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}
// Helper function: Parse palette string (comma-separated hex colors)
function parsePalette(pString) {
const colors = pString.split(',');
if (colors.length === 0 || pString.trim() === "") {
// Default to Black & White if palette string is empty
return [{ r: 0, g: 0, b: 0 }, { r: 255, g: 255, b: 255 }];
}
return colors.map(hex => {
hex = hex.trim().replace('#', '');
if (hex.length === 3) { // Handle shorthand hex like #RGB
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
if (hex.length !== 6) {
return { r: 0, g: 0, b: 0 }; // Default to black on invalid length
}
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
if (isNaN(r) || isNaN(g) || isNaN(b)) {
return { r: 0, g: 0, b: 0 }; // Default to black on parsing error
}
return { r, g, b };
});
}
// Helper function: Find closest color in palette
function findClosestColor(r, g, b, currentPalette) {
let closestColor = currentPalette[0];
let minDistanceSquared = Infinity;
for (const color of currentPalette) {
const distR = r - color.r;
const distG = g - color.g;
const distB = b - color.b;
const distanceSquared = distR * distR + distG * distG + distB * distB;
if (distanceSquared < minDistanceSquared) {
minDistanceSquared = distanceSquared;
closestColor = color;
}
}
return closestColor;
}
// Bayer matrices definition
const bayerMatrices = {
2: [
[0, 2],
[3, 1]
],
4: [
[ 0, 8, 2, 10],
[12, 4, 14, 6],
[ 3, 11, 1, 9],
[15, 7, 13, 5]
],
8: [
[ 0, 32, 8, 40, 2, 34, 10, 42],
[48, 16, 56, 24, 50, 18, 58, 26],
[12, 44, 4, 36, 14, 46, 6, 38],
[60, 28, 52, 20, 62, 30, 54, 22],
[ 3, 35, 11, 43, 1, 33, 9, 41],
[51, 19, 59, 27, 49, 17, 57, 25],
[15, 47, 7, 39, 13, 45, 5, 37],
[63, 31, 55, 23, 61, 29, 53, 21]
]
};
function getBayerMatrix(size) {
if (bayerMatrices[size]) {
return bayerMatrices[size];
}
console.warn(`Bayer matrix of size ${size} not available. Using size 4.`);
return bayerMatrices[4];
}
// Dithering Algorithm: Threshold
function applyThreshold(imgData, imgWidth, imgHeight, currentPalette, currentThresholdVal) {
const data = imgData.data;
const color0 = currentPalette[0] || {r:0, g:0, b:0}; // Default to black if palette is empty
const color1 = currentPalette[1] || (currentPalette[0] || {r:255,g:255,b:255}); // Default to white or first color
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
let newColor;
if (luminance < currentThresholdVal) {
newColor = color0;
} else {
newColor = color1;
}
data[i] = newColor.r;
data[i + 1] = newColor.g;
data[i + 2] = newColor.b;
}
}
// Dithering Algorithm: Floyd-Steinberg
function applyFloydSteinberg(imgData, imgWidth, imgHeight, currentPalette) {
const data = imgData.data;
const floatData = new Float32Array(imgWidth * imgHeight * 3);
for (let y = 0; y < imgHeight; y++) {
for (let x = 0; x < imgWidth; x++) {
const i = (y * imgWidth + x) * 4;
const fIdx = (y * imgWidth + x) * 3;
floatData[fIdx] = data[i]; // R
floatData[fIdx + 1] = data[i + 1]; // G
floatData[fIdx + 2] = data[i + 2]; // B
}
}
for (let y = 0; y < imgHeight; y++) {
for (let x = 0; x < imgWidth; x++) {
const fIdx = (y * imgWidth + x) * 3;
const oldR = floatData[fIdx];
const oldG = floatData[fIdx + 1];
const oldB = floatData[fIdx + 2];
const newColor = findClosestColor(oldR, oldG, oldB, currentPalette);
const dataIdx = (y * imgWidth + x) * 4;
data[dataIdx] = newColor.r;
data[dataIdx + 1] = newColor.g;
data[dataIdx + 2] = newColor.b;
const errR = oldR - newColor.r;
const errG = oldG - newColor.g;
const errB = oldB - newColor.b;
// Distribute error (Indices are for floatData)
// Right: (x+1, y)
if (x + 1 < imgWidth) {
const i = (y * imgWidth + (x + 1)) * 3;
floatData[i] = floatData[i] + errR * 7 / 16;
floatData[i + 1] = floatData[i+1] + errG * 7 / 16;
floatData[i + 2] = floatData[i+2] + errB * 7 / 16;
}
// Bottom-left: (x-1, y+1)
if (x - 1 >= 0 && y + 1 < imgHeight) {
const i = ((y + 1) * imgWidth + (x - 1)) * 3;
floatData[i] = floatData[i] + errR * 3 / 16;
floatData[i + 1] = floatData[i+1] + errG * 3 / 16;
floatData[i + 2] = floatData[i+2] + errB * 3 / 16;
}
// Bottom: (x, y+1)
if (y + 1 < imgHeight) {
const i = ((y + 1) * imgWidth + x) * 3;
floatData[i] = floatData[i] + errR * 5 / 16;
floatData[i + 1] = floatData[i+1] + errG * 5 / 16;
floatData[i + 2] = floatData[i+2] + errB * 5 / 16;
}
// Bottom-right: (x+1, y+1)
if (x + 1 < imgWidth && y + 1 < imgHeight) {
const i = ((y + 1) * imgWidth + (x + 1)) * 3;
floatData[i] = floatData[i] + errR * 1 / 16;
floatData[i + 1] = floatData[i+1] + errG * 1 / 16;
floatData[i + 2] = floatData[i+2] + errB * 1 / 16;
}
}
}
}
// Dithering Algorithm: Bayer (Ordered Dithering)
function applyBayerDithering(imgData, imgWidth, imgHeight, currentPalette, matrixSize, levels) {
const data = imgData.data;
const bayerMatrix = getBayerMatrix(matrixSize);
const M_sq = matrixSize * matrixSize;
const bayerThresholds = bayerMatrix.map(row => row.map(val => val / M_sq)); // Normalized to [0,1)
if (levels < 1) levels = 1; // Prevent division by zero if levels is < 1
for (let y = 0; y < imgHeight; y++) {
for (let x = 0; x < imgWidth; x++) {
const i = (y * imgWidth + x) * 4;
const r_orig = data[i];
const g_orig = data[i + 1];
const b_orig = data[i + 2];
const threshold = bayerThresholds[y % matrixSize][x % matrixSize];
let r_temp, g_temp, b_temp;
if (levels === 1) { // Quantize to a single level (average or first color)
// This means the intermediate color is fixed before picking from palette
r_temp = 128; g_temp = 128; b_temp = 128; // Or use 0, or average of palette, behavior can be tuned
} else {
// Apply Bayer formula to get N-level intermediate color components
r_temp = Math.floor( (r_orig / 255.0) * (levels - 1) + threshold ) / (levels - 1) * 255.0;
g_temp = Math.floor( (g_orig / 255.0) * (levels - 1) + threshold ) / (levels - 1) * 255.0;
b_temp = Math.floor( (b_orig / 255.0) * (levels - 1) + threshold ) / (levels - 1) * 255.0;
}
// Clamp intermediate values (should inherently be within range, but good practice)
r_temp = clamp(r_temp, 0, 255);
g_temp = clamp(g_temp, 0, 255);
b_temp = clamp(b_temp, 0, 255);
// Find the closest color in the final palette for this dithered intermediate color
const newColor = findClosestColor(r_temp, g_temp, b_temp, currentPalette);
data[i] = newColor.r;
data[i + 1] = newColor.g;
data[i + 2] = newColor.b;
}
}
}
// Main function logic
const canvas = document.createElement('canvas');
const width = originalImg.naturalWidth || originalImg.width;
const height = originalImg.naturalHeight || originalImg.height;
canvas.width = width;
canvas.height = height;
if (width === 0 || height === 0) {
return canvas; // Return empty (0x0) canvas if image has no dimensions
}
const ctx = canvas.getContext('2d');
ctx.drawImage(originalImg, 0, 0, width, height);
const imageData = ctx.getImageData(0, 0, width, height);
const finalPalette = parsePalette(paletteString);
if (finalPalette.length === 0) { // Should be handled by parsePalette, but as a safeguard
console.warn("Palette is empty. Using default black and white.");
finalPalette.push({r:0,g:0,b:0}, {r:255,g:255,b:255});
}
const algo = algorithm.toLowerCase();
if (algo === "floydsteinberg") {
applyFloydSteinberg(imageData, width, height, finalPalette);
} else if (algo === "bayer") {
applyBayerDithering(imageData, width, height, finalPalette, bayerMatrixSize, bayerLevels);
} else if (algo === "threshold") {
applyThreshold(imageData, width, height, finalPalette, thresholdVal);
} else {
console.warn(`Unknown algorithm: ${algorithm}. Defaulting to FloydSteinberg.`);
applyFloydSteinberg(imageData, width, height, finalPalette);
}
ctx.putImageData(imageData, 0, 0);
return canvas;
}
Apply Changes