You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, numColors = 16, blockSize = 4, dithering = 'bayer') {
// Coerce parameters to their expected types
const colorCount = Math.max(2, parseInt(numColors, 10));
const pixelSize = Math.max(1, parseInt(blockSize, 10));
const ditherType = String(dithering).toLowerCase();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const w = originalImg.width;
const h = originalImg.height;
canvas.width = w;
canvas.height = h;
// --- 1. Pixelation Step ---
// Downsample the image to create a blocky effect, then scale it back up
// using nearest-neighbor scaling (imageSmoothingEnabled = false).
if (pixelSize > 1) {
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
const smallW = Math.ceil(w / pixelSize);
const smallH = Math.ceil(h / pixelSize);
tempCanvas.width = smallW;
tempCanvas.height = smallH;
// Draw the original image into the small canvas, effectively downsampling it.
tempCtx.drawImage(originalImg, 0, 0, smallW, smallH);
// Disable smoothing to get sharp, crisp pixels when scaling up.
ctx.imageSmoothingEnabled = false;
// Draw the small canvas back onto the main canvas, scaled up.
ctx.drawImage(tempCanvas, 0, 0, smallW, smallH, 0, 0, w, h);
} else {
ctx.drawImage(originalImg, 0, 0);
}
// --- 2. Color Reduction and Dithering Step ---
const imageData = ctx.getImageData(0, 0, w, h);
const data = imageData.data;
// --- Helper Function: Generate a color palette ---
// Creates a generic RGB color cube palette.
const generatePalette = (count) => {
const palette = [];
const levels = Math.round(Math.cbrt(count));
const step = 255 / (levels - 1);
if (levels <= 1) {
return [[0, 0, 0], [255, 255, 255]];
}
for (let r = 0; r < levels; r++) {
for (let g = 0; g < levels; g++) {
for (let b = 0; b < levels; b++) {
palette.push([
Math.round(r * step),
Math.round(g * step),
Math.round(b * step)
]);
}
}
}
return palette;
};
const palette = generatePalette(colorCount);
// --- Helper Function: Find the closest color in the palette ---
// Uses squared Euclidean distance for efficiency (avoids square roots).
const findClosestColor = (r, g, b, palette) => {
let minDistanceSq = Infinity;
let closestColor = palette[0];
for (const color of palette) {
const dR = r - color[0];
const dG = g - color[1];
const dB = b - color[2];
const distanceSq = dR * dR + dG * dG + dB * dB;
if (distanceSq < minDistanceSq) {
minDistanceSq = distanceSq;
closestColor = color;
}
}
return closestColor;
};
// --- Dithering Matrix (for Bayer dithering) ---
// A 4x4 ordered dithering matrix.
const bayerMatrix = [
[0, 8, 2, 10],
[12, 4, 14, 6],
[3, 11, 1, 9],
[15, 7, 13, 5]
];
const bayerSize = 4;
const ditherFactor = 48; // A magic number to control dither intensity
// --- Process each pixel ---
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i + 1];
let b = data[i + 2];
// Apply Bayer dithering if selected
if (ditherType === 'bayer') {
const x = (i / 4) % w;
const y = Math.floor((i / 4) / w);
const bayerValue = bayerMatrix[y % bayerSize][x % bayerSize];
const threshold = (bayerValue / (bayerSize * bayerSize) - 0.5) * ditherFactor;
r = Math.max(0, Math.min(255, r + threshold));
g = Math.max(0, Math.min(255, g + threshold));
b = Math.max(0, Math.min(255, b + threshold));
}
// Find the closest color from our generated palette
const newColor = findClosestColor(r, g, b, palette);
// Update the image data with the new color
data[i] = newColor[0];
data[i + 1] = newColor[1];
data[i + 2] = newColor[2];
// Alpha channel (data[i + 3]) is left untouched.
}
// --- 3. Final Step: Put modified data back ---
ctx.putImageData(imageData, 0, 0);
return canvas;
}
Apply Changes