You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, pixelationLevel = 8, dithering = 'floyd-steinberg', textOverlay = '', fontColor = '0,0,0', fontSize = 24, textX = 10, textY = 10) {
/**
* Dynamically loads a Google Font to be used on the canvas.
* Uses "VT323" for a retro, pixelated, MS Paint-like feel.
* @param {string} fontFamily - The name of the font family to load.
* @param {number} size - The font size to use for loading check.
*/
const loadFont = async (fontFamily, size) => {
const fontId = `font-loader-${fontFamily.replace(/\s/g, '-')}`;
if (document.getElementById(fontId)) {
// Font stylesheet is already added, assume it's loaded or loading
await document.fonts.load(`${size}px ${fontFamily}`);
return;
}
const link = document.createElement('link');
link.id = fontId;
link.rel = 'stylesheet';
link.href = `https://fonts.googleapis.com/css2?family=${fontFamily.replace(/\s/g, '+')}&display=swap`;
const fontPromise = document.fonts.load(`${size}px ${fontFamily}`);
document.head.appendChild(link);
await fontPromise;
};
// --- 1. SETUP & PARAMETER VALIDATION ---
const FONT_FAMILY = 'VT323';
if (textOverlay && textOverlay.trim() !== '') {
await loadFont(FONT_FAMILY, fontSize);
}
const w = originalImg.width;
const h = originalImg.height;
const blocksize = Math.max(1, Math.floor(pixelationLevel));
// The iconic 16-color Windows / VGA palette
const palette = [
[0, 0, 0], [128, 0, 0], [0, 128, 0], [128, 128, 0],
[0, 0, 128], [128, 0, 128], [0, 128, 128], [192, 192, 192],
[128, 128, 128], [255, 0, 0], [0, 255, 0], [255, 255, 0],
[0, 0, 255], [255, 0, 255], [0, 255, 255], [255, 255, 255]
];
/**
* Finds the closest color in the palette to a given RGB color.
* Uses squared Euclidean distance for efficiency.
*/
const findClosestColor = (r, g, b, pal) => {
let closest = pal[0];
let minDistance = Infinity;
for (const color of pal) {
const dist = (r - color[0]) ** 2 + (g - color[1]) ** 2 + (b - color[2]) ** 2;
if (dist < minDistance) {
minDistance = dist;
closest = color;
}
}
return closest;
};
// --- 2. IMAGE PROCESSING (PIXELATION & DITHERING) ---
// Create a working canvas, downsampled if pixelation is requested.
// Downsampling first improves performance of the dithering algorithm.
const workingCanvas = document.createElement('canvas');
const workingCtx = workingCanvas.getContext('2d');
workingCanvas.width = Math.floor(w / blocksize);
workingCanvas.height = Math.floor(h / blocksize);
// Draw the original image onto the smaller canvas, which averages the pixels.
workingCtx.drawImage(originalImg, 0, 0, workingCanvas.width, workingCanvas.height);
const imageData = workingCtx.getImageData(0, 0, workingCanvas.width, workingCanvas.height);
const pixels = imageData.data;
// Create a floating-point representation of the image for accurate error diffusion.
const floatPixels = [];
for (let i = 0; i < pixels.length; i += 4) {
floatPixels.push({ r: pixels[i], g: pixels[i + 1], b: pixels[i + 2] });
}
// Apply color quantization and optional dithering.
for (let y = 0; y < workingCanvas.height; y++) {
for (let x = 0; x < workingCanvas.width; x++) {
const index = y * workingCanvas.width + x;
const oldColor = floatPixels[index];
const newColor = findClosestColor(oldColor.r, oldColor.g, oldColor.b, palette);
// Update the final pixel data with the new quantized color.
pixels[index * 4] = newColor[0];
pixels[index * 4 + 1] = newColor[1];
pixels[index * 4 + 2] = newColor[2];
if (dithering.toLowerCase() === 'floyd-steinberg') {
const errR = oldColor.r - newColor[0];
const errG = oldColor.g - newColor[1];
const errB = oldColor.b - newColor[2];
// Distribute the error to neighboring pixels.
const distributeError = (dx, dy, factor) => {
const nx = x + dx;
const ny = y + dy;
if (nx >= 0 && nx < workingCanvas.width && ny >= 0 && ny < workingCanvas.height) {
const neighborIndex = ny * workingCanvas.width + nx;
floatPixels[neighborIndex].r += errR * factor;
floatPixels[neighborIndex].g += errG * factor;
floatPixels[neighborIndex].b += errB * factor;
}
};
distributeError(1, 0, 7 / 16);
distributeError(-1, 1, 3 / 16);
distributeError(0, 1, 5 / 16);
distributeError(1, 1, 1 / 16);
}
}
}
// Put the processed pixel data back onto the working canvas.
workingCtx.putImageData(imageData, 0, 0);
// --- 3. FINAL COMPOSITION & TEXT OVERLAY ---
// Create the final output canvas at the original size.
const outputCanvas = document.createElement('canvas');
outputCanvas.width = w;
outputCanvas.height = h;
const outputCtx = outputCanvas.getContext('2d');
// Disable anti-aliasing to preserve the sharp, blocky pixel look.
outputCtx.imageSmoothingEnabled = false;
outputCtx.mozImageSmoothingEnabled = false;
outputCtx.webkitImageSmoothingEnabled = false;
outputCtx.msImageSmoothingEnabled = false;
// Draw the processed (and possibly smaller) working canvas onto the final canvas, scaling it up.
outputCtx.drawImage(workingCanvas, 0, 0, w, h);
// Add the text overlay if provided.
if (textOverlay && textOverlay.trim() !== '') {
outputCtx.font = `${fontSize}px "${FONT_FAMILY}"`;
outputCtx.fillStyle = `rgb(${fontColor})`;
outputCtx.textBaseline = 'top';
outputCtx.fillText(textOverlay, textX, textY);
}
return outputCanvas;
}
Apply Changes