You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, posterizeLevels = 6, edgeThreshold = 80, edgeColorStr = "#3A271C", edgeThickness = 1) {
let width = originalImg.naturalWidth || originalImg.width;
let height = originalImg.naturalHeight || originalImg.height;
// Handle cases where the image might not be fully loaded yet (common for HTMLImageElement)
if ((!width || !height) && (originalImg instanceof HTMLImageElement && !originalImg.complete && originalImg.src)) {
try {
await new Promise((resolve, reject) => {
const loadHandler = () => {
originalImg.removeEventListener('load', loadHandler);
originalImg.removeEventListener('error', errorHandler);
resolve();
};
const errorHandler = (err) => {
originalImg.removeEventListener('load', loadHandler);
originalImg.removeEventListener('error', errorHandler);
reject(err);
};
originalImg.addEventListener('load', loadHandler);
originalImg.addEventListener('error', errorHandler);
// If image already has src and loading has started, these listeners will catch it.
// If src is set *after* this function call, this won't help unless processImage is called again.
});
width = originalImg.naturalWidth;
height = originalImg.naturalHeight;
} catch (e) {
console.error("Error loading image:", e);
// Fall through to create a minimal error canvas
}
}
// If dimensions are still invalid, return a minimal canvas
if (!width || !height) {
console.error("Image has invalid dimensions or failed to load.");
const errCanvas = document.createElement('canvas');
errCanvas.width = 1; errCanvas.height = 1;
const errCtx = errCanvas.getContext('2d');
if (errCtx) { // Make it visibly an error if possible
errCtx.fillStyle = 'red';
errCtx.fillRect(0,0,1,1);
}
return errCanvas;
}
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
ctx.drawImage(originalImg, 0, 0, width, height);
const originalImageData = ctx.getImageData(0, 0, width, height);
const data = originalImageData.data;
// 1. Posterize Image
const posterizedImageData = ctx.createImageData(width, height);
const posterizedData = posterizedImageData.data;
const numLevels = Math.max(2, Math.min(255, Math.floor(posterizeLevels)));
const step = (numLevels <= 1) ? 256 : 255 / (numLevels - 1); // Avoid division by zero if numLevels is 1
for (let i = 0; i < data.length; i += 4) {
if (numLevels <= 1) { // effectively no change or single color (though Math.max(2,..) prevents this)
posterizedData[i] = data[i];
posterizedData[i+1] = data[i+1];
posterizedData[i+2] = data[i+2];
} else {
posterizedData[i] = Math.min(255, Math.round(data[i] / step) * step);
posterizedData[i+1] = Math.min(255, Math.round(data[i+1] / step) * step);
posterizedData[i+2] = Math.min(255, Math.round(data[i+2] / step) * step);
}
posterizedData[i+3] = data[i+3]; // Preserve alpha
}
// 2. Grayscale Data for Edge Detection
const grayData = new Uint8ClampedArray(width * height);
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i+1];
const b = data[i+2];
grayData[i / 4] = 0.299 * r + 0.587 * g + 0.114 * b; // Luminosity
}
// 3. Sobel Edge Detection
const edgeMap = new Uint8Array(width * height).fill(0);
const Gx = [
[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]
];
const Gy = [
[-1, -2, -1],
[0, 0, 0],
[1, 2, 1]
];
const clampedEdgeThreshold = Math.max(0, Math.min(255, edgeThreshold));
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
let sumX = 0;
let sumY = 0;
for (let ky = -1; ky <= 1; ky++) {
for (let kx = -1; kx <= 1; kx++) {
const pixelVal = grayData[(y + ky) * width + (x + kx)];
sumX += pixelVal * Gx[ky + 1][kx + 1];
sumY += pixelVal * Gy[ky + 1][kx + 1];
}
}
const magnitude = Math.sqrt(sumX * sumX + sumY * sumY);
if (magnitude > clampedEdgeThreshold) {
edgeMap[y * width + x] = 1;
}
}
}
// 4. Combine on output canvas
// Draw posterized image first
ctx.putImageData(posterizedImageData, 0, 0);
// Draw edges on top
ctx.fillStyle = edgeColorStr;
const finalEdgeThickness = Math.max(1, Math.floor(edgeThickness));
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (edgeMap[y * width + x] === 1) {
// Center the thicker line block around the detected edge pixel
const rectX = x - Math.floor(finalEdgeThickness / 2);
const rectY = y - Math.floor(finalEdgeThickness / 2);
ctx.fillRect(rectX, rectY, finalEdgeThickness, finalEdgeThickness);
}
}
}
return canvas;
}
Apply Changes