You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, posterizeLevels = 4, saturation = 1.2, pixelSize = 1, palette = "") {
// Ensure parameters are numbers and properly initialized
posterizeLevels = Number(posterizeLevels);
saturation = Number(saturation);
pixelSize = Number(pixelSize);
// Validate and adjust parameters
let pLevels = Math.floor(posterizeLevels); // Used for posterization logic
pixelSize = Math.max(1, Math.floor(pixelSize)); // pixelSize must be an integer >= 1
const outputCanvas = document.createElement('canvas');
const ctx = outputCanvas.getContext('2d');
const w = originalImg.naturalWidth || originalImg.width;
const h = originalImg.naturalHeight || originalImg.height;
outputCanvas.width = w;
outputCanvas.height = h;
// --- Helper Function: Hex to RGB ---
function hexToRgb(hex) {
if (!hex) return null;
hex = hex.replace(/^#/, '');
if (!/^(?:[0-9a-fA-F]{3}){1,2}$/.test(hex)) {
// console.warn(`Invalid hex format: ${hex}`); // Optional: for debugging
return null;
}
let r, g, b;
if (hex.length === 3) {
r = parseInt(hex[0] + hex[0], 16);
g = parseInt(hex[1] + hex[1], 16);
b = parseInt(hex[2] + hex[2], 16);
} else { // length is 6
r = parseInt(hex.substring(0, 2), 16);
g = parseInt(hex.substring(2, 4), 16);
b = parseInt(hex.substring(4, 6), 16);
}
return [r, g, b];
}
// --- Helper Function: 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: 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)];
}
// --- Helper Function: Color Distance (squared for efficiency) ---
function colorDistanceSquared(rgb1, rgb2) {
const dr = rgb1[0] - rgb2[0];
const dg = rgb1[1] - rgb2[1];
const db = rgb1[2] - rgb2[2];
return dr * dr + dg * dg + db * db;
}
// --- Parse custom palette ---
let parsedPalette = [];
if (typeof palette === 'string' && palette.trim() !== "") {
const colors = palette.split(',');
for (const colorStr of colors) {
const rgb = hexToRgb(colorStr.trim());
if (rgb) {
parsedPalette.push(rgb);
} else {
// console.warn(`Invalid color in palette string: ${colorStr}`); // Optional: for debugging
}
}
}
// --- 1. Prepare initial image on canvas (with pixelation if needed) ---
if (w > 0 && h > 0) { // Only proceed if image has dimensions
if (pixelSize > 1) {
const tempPixelCanvas = document.createElement('canvas');
const tempPixelCtx = tempPixelCanvas.getContext('2d');
const smallWidth = Math.max(1, Math.floor(w / pixelSize));
const smallHeight = Math.max(1, Math.floor(h / pixelSize));
tempPixelCanvas.width = smallWidth;
tempPixelCanvas.height = smallHeight;
// Draw original image downscaled into the temporary canvas
tempPixelCtx.drawImage(originalImg, 0, 0, smallWidth, smallHeight);
// Now draw from the small temporary canvas to the main outputCanvas, scaled up
ctx.imageSmoothingEnabled = false;
ctx.mozImageSmoothingEnabled = false; // Firefox
ctx.webkitImageSmoothingEnabled = false; // Chrome, Safari, Opera
ctx.msImageSmoothingEnabled = false; // IE, Edge
ctx.drawImage(tempPixelCanvas, 0, 0, smallWidth, smallHeight, 0, 0, w, h);
} else {
// No pixelation, draw original image directly onto outputCanvas
ctx.drawImage(originalImg, 0, 0, w, h);
}
} else {
// Image has no dimensions, return the empty (possibly 0x0) canvas
return outputCanvas;
}
const imageData = ctx.getImageData(0, 0, w, h);
const data = imageData.data;
// --- 2. 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];
// --- 2a. Saturation ---
if (saturation !== 1.0 && saturation >= 0) { // Allow desaturation (saturation=0)
const hsl = rgbToHsl(r, g, b);
hsl[1] *= saturation;
hsl[1] = Math.max(0, Math.min(1, hsl[1])); // Clamp Saturation to [0, 1]
const saturatedRgb = hslToRgb(hsl[0], hsl[1], hsl[2]);
r = saturatedRgb[0];
g = saturatedRgb[1];
b = saturatedRgb[2];
}
// --- 2b. Posterization / Palette Quantization ---
if (parsedPalette.length > 0) {
// Quantize to custom palette
let minDistSq = Infinity;
let closestColor = parsedPalette[0]; // Default to first color in palette
for (const paletteRgb of parsedPalette) {
const distSq = colorDistanceSquared([r, g, b], paletteRgb);
if (distSq < minDistSq) {
minDistSq = distSq;
closestColor = paletteRgb;
}
if (minDistSq === 0) break; // Exact match found, no need to check further
}
r = closestColor[0];
g = closestColor[1];
b = closestColor[2];
} else if (pLevels >= 2) {
// Apply posterization if no custom palette and pLevels is valid
const step = 255 / (pLevels - 1);
r = Math.round(r / step) * step;
g = Math.round(g / step) * step;
b = Math.round(b / step) * step;
}
// Assign processed and clamped color values
data[i] = Math.min(255, Math.max(0, Math.round(r)));
data[i + 1] = Math.min(255, Math.max(0, Math.round(g)));
data[i + 2] = Math.min(255, Math.max(0, Math.round(b)));
// Alpha channel (data[i+3]) remains unchanged
}
// --- 3. Put modified ImageData back to canvas ---
ctx.putImageData(imageData, 0, 0);
return outputCanvas;
}
Apply Changes