You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, posterizationLevelsParam = 4, outlineStrengthParam = 120, saturationBoostParam = 1.2) {
const posterizationLevels = Math.max(2, Math.floor(Number(posterizationLevelsParam)));
const outlineStrength = Number(outlineStrengthParam);
const saturationBoost = Number(saturationBoostParam);
// Helper function to convert RGB to HSL
function rgbToHsl(r, g, b) {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0; // achromatic
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return [h, s, l];
}
// Helper function to convert HSL to RGB
function hslToRgb(h, s, l) {
let r, g, b;
if (s === 0) {
r = g = b = l; // achromatic
} else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true }); // Optimization hint
const imgWidth = originalImg.naturalWidth || originalImg.width;
const imgHeight = originalImg.naturalHeight || originalImg.height;
canvas.width = imgWidth;
canvas.height = imgHeight;
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
let imageData;
try {
imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
} catch (e) {
console.error("Error getting ImageData: ", e);
// Fallback: draw an error message on the canvas
ctx.clearRect(0,0,canvas.width, canvas.height);
ctx.fillStyle = "rgba(200, 0, 0, 0.7)";
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.fillStyle = "white";
ctx.font = "16px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const lines = `Error processing image.
This might be due to cross-origin restrictions if the image isn't hosted on the same domain.
Details: ${e.message}`.split('\n');
lines.forEach((line, index) => {
ctx.fillText(line, canvas.width / 2, canvas.height / 2 - (lines.length-1)*10 + index*20);
});
return canvas;
}
const data = imageData.data;
const posterizedDataArray = new Uint8ClampedArray(data.length);
const posterizeFactor = 255 / (posterizationLevels - 1);
// --- Stage 1: Saturation Boost and Posterization ---
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i + 1];
let b = data[i + 2];
if (saturationBoost !== 1.0) {
let [h, s, l] = rgbToHsl(r, g, b);
s = Math.max(0, Math.min(1, s * saturationBoost)); // Boost/reduce saturation, clamp to [0, 1]
[r, g, b] = hslToRgb(h, s, l);
}
posterizedDataArray[i] = Math.round(r / posterizeFactor) * posterizeFactor;
posterizedDataArray[i + 1] = Math.round(g / posterizeFactor) * posterizeFactor;
posterizedDataArray[i + 2] = Math.round(b / posterizeFactor) * posterizeFactor;
posterizedDataArray[i + 3] = data[i + 3];
}
const finalData = new Uint8ClampedArray(data.length);
const width = canvas.width;
const height = canvas.height;
const Gx_kernel = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]];
const Gy_kernel = [[-1, -2, -1], [0, 0, 0], [1, 2, 1]];
// --- Stage 2: Outline Detection (Sobel on grayscale of posterized image) ---
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const idx = (y * width + x) * 4;
let gx_lum = 0;
let gy_lum = 0;
for (let ky = -1; ky <= 1; ky++) {
for (let kx = -1; kx <= 1; kx++) {
const nX = Math.max(0, Math.min(width - 1, x + kx));
const nY = Math.max(0, Math.min(height - 1, y + ky));
const nIdx = (nY * width + nX) * 4;
const rN = posterizedDataArray[nIdx];
const gN = posterizedDataArray[nIdx + 1];
const bN = posterizedDataArray[nIdx + 2];
const luminance = 0.299 * rN + 0.587 * gN + 0.114 * bN;
gx_lum += luminance * Gx_kernel[ky + 1][kx + 1];
gy_lum += luminance * Gy_kernel[ky + 1][kx + 1];
}
}
const magnitude = Math.sqrt(gx_lum * gx_lum + gy_lum * gy_lum);
if (magnitude > outlineStrength) {
finalData[idx] = 0; // Black outline
finalData[idx + 1] = 0;
finalData[idx + 2] = 0;
} else {
finalData[idx] = posterizedDataArray[idx];
finalData[idx + 1] = posterizedDataArray[idx + 1];
finalData[idx + 2] = posterizedDataArray[idx + 2];
}
finalData[idx + 3] = posterizedDataArray[idx + 3]; // Alpha
}
}
data.set(finalData);
ctx.putImageData(imageData, 0, 0);
return canvas;
}
Apply Changes