You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
outlineThreshold = 60,
outlineColorStr = "black",
posterizeLevels = 5,
enableHalftoneStr = "true",
halftoneGridSize = 8,
halftoneMaxDotRadius = 3,
halftoneDotColorStr = "rgba(0,0,0,0.3)"
) {
// 1. Parameter Parsing and Validation
const enableHalftone = String(enableHalftoneStr).toLowerCase() === 'true' || String(enableHalftoneStr) === '1';
posterizeLevels = Math.max(2, Math.floor(Number(posterizeLevels)));
outlineThreshold = Number(outlineThreshold);
halftoneGridSize = Math.max(1, Math.floor(Number(halftoneGridSize)));
halftoneMaxDotRadius = Math.max(0, Number(halftoneMaxDotRadius));
const width = originalImg.naturalWidth || originalImg.width;
const height = originalImg.naturalHeight || originalImg.height;
// 2. Canvas Setup
const outputCanvas = document.createElement('canvas');
outputCanvas.width = width;
outputCanvas.height = height;
const ctx = outputCanvas.getContext('2d');
if (!ctx) {
console.error("Could not get 2D context from output canvas.");
return outputCanvas; // Return empty canvas or throw error
}
const tempCanvas = document.createElement('canvas');
tempCanvas.width = width;
tempCanvas.height = height;
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
if (!tempCtx) {
console.error("Could not get 2D context from temporary canvas.");
ctx.drawImage(originalImg, 0, 0, width, height); // Draw original as fallback
return outputCanvas;
}
tempCtx.drawImage(originalImg, 0, 0, width, height);
let originalImageData;
try {
originalImageData = tempCtx.getImageData(0, 0, width, height);
} catch (e) {
console.error("Error getting image data. This might be due to cross-origin restrictions if the image isn't hosted on the same domain or lacks CORS headers.", e);
// Fallback: draw original image and return
ctx.drawImage(originalImg, 0, 0, width, height);
return outputCanvas;
}
const originalPixels = originalImageData.data;
// Helper function for color parsing
function getRGBA(colorStr) {
const c = document.createElement('canvas');
c.width = 1;
c.height = 1;
const tinyCtx = c.getContext('2d', { willReadFrequently: true });
if (!tinyCtx) return { r:0, g:0, b:0, a:255 }; // Fallback color
tinyCtx.fillStyle = colorStr;
tinyCtx.fillRect(0, 0, 1, 1);
const data = tinyCtx.getImageData(0, 0, 1, 1).data;
return { r: data[0], g: data[1], b: data[2], a: data[3] };
}
// 3. Posterization
const posterizedPixels = new Uint8ClampedArray(originalPixels.length);
if (posterizeLevels <= 1) posterizeLevels = 2; // Should be caught by Math.max earlier
const سلم = 255 / (posterizeLevels - 1); // 'سلم' (sullam) means 'ladder' or 'step' in Arabic
for (let i = 0; i < originalPixels.length; i += 4) {
posterizedPixels[i] = Math.round(originalPixels[i] / سلم) * سلم;
posterizedPixels[i+1] = Math.round(originalPixels[i+1] / سلم) * سلم;
posterizedPixels[i+2] = Math.round(originalPixels[i+2] / سلم) * سلم;
posterizedPixels[i+3] = originalPixels[i+3]; // Alpha
}
ctx.putImageData(new ImageData(posterizedPixels, width, height), 0, 0);
// 4. Halftone Effect (if enabled)
if (enableHalftone && halftoneMaxDotRadius > 0 && halftoneGridSize > 0) {
ctx.fillStyle = halftoneDotColorStr;
for (let y = 0; y < height; y += halftoneGridSize) {
for (let x = 0; x < width; x += halftoneGridSize) {
let sumLuminance = 0;
let numBlockPixels = 0;
const blockStartX = x;
const blockStartY = y;
const blockEndX = Math.min(width, x + halftoneGridSize);
const blockEndY = Math.min(height, y + halftoneGridSize);
for (let by = blockStartY; by < blockEndY; by++) {
for (let bx = blockStartX; bx < blockEndX; bx++) {
const idx = (by * width + bx) * 4;
const r = posterizedPixels[idx];
const g = posterizedPixels[idx+1];
const b = posterizedPixels[idx+2];
sumLuminance += (0.299 * r + 0.587 * g + 0.114 * b);
numBlockPixels++;
}
}
if (numBlockPixels > 0) {
const avgLuminance = sumLuminance / numBlockPixels;
const dotRadius = (1 - (avgLuminance / 255)) * halftoneMaxDotRadius;
if (dotRadius > 0.1) { // Avoid drawing tiny/invisible dots
ctx.beginPath();
ctx.arc(
x + halftoneGridSize / 2,
y + halftoneGridSize / 2,
dotRadius,
0,
2 * Math.PI
);
ctx.fill();
}
}
}
}
}
// 5. Outlines (Sobel)
const grayscaleMap = new Uint8Array(width * height);
for (let i = 0; i < posterizedPixels.length; i += 4) {
const r = posterizedPixels[i];
const g = posterizedPixels[i+1];
const b = posterizedPixels[i+2];
grayscaleMap[i / 4] = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
}
const outlinePixelData = new Uint8ClampedArray(originalPixels.length).fill(0); // Initialize with transparent
const outlineActualColor = getRGBA(outlineColorStr);
const kernelX = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]];
const kernelY = [[-1, -2, -1], [0, 0, 0], [1, 2, 1]];
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
let gx = 0;
let gy = 0;
for (let ky = -1; ky <= 1; ky++) {
for (let kx = -1; kx <= 1; kx++) {
const R_idx = ((y + ky) * width + (x + kx));
const grayVal = grayscaleMap[R_idx];
gx += grayVal * kernelX[ky + 1][kx + 1];
gy += grayVal * kernelY[ky + 1][kx + 1];
}
}
const magnitude = Math.sqrt(gx * gx + gy * gy);
if (magnitude > outlineThreshold) {
const idx = (y * width + x) * 4;
outlinePixelData[idx] = outlineActualColor.r;
outlinePixelData[idx + 1] = outlineActualColor.g;
outlinePixelData[idx + 2] = outlineActualColor.b;
outlinePixelData[idx + 3] = outlineActualColor.a;
}
}
}
const outlineCanvas = document.createElement('canvas');
outlineCanvas.width = width;
outlineCanvas.height = height;
const outlineCtx = outlineCanvas.getContext('2d');
if (outlineCtx) {
outlineCtx.putImageData(new ImageData(outlinePixelData, width, height), 0, 0);
ctx.drawImage(outlineCanvas, 0, 0);
} else {
console.error("Could not get 2D context for outline canvas.");
}
return outputCanvas;
}
Apply Changes