You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, style = 'oil', intensity = 5, colors = '#003366,#ffcc00,#ff00ff,#00ff00') {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
canvas.width = originalImg.naturalWidth || originalImg.width;
canvas.height = originalImg.naturalHeight || originalImg.height;
// Draw the original image to the canvas to work with it
ctx.drawImage(originalImg, 0, 0);
// --- Helper Functions defined inside the main function for encapsulation ---
/**
* Applies an oil painting effect using a Kuwahara filter. This filter reduces noise
* while preserving edges, creating a painterly look by replacing a pixel's value
* with the mean of the quadrant in its neighborhood with the least variance.
* @param {CanvasRenderingContext2D} ctx - The canvas context.
* @param {number} width - The width of the canvas.
* @param {number} height - The height of the canvas.
* @param {number} radius - The radius of the filter kernel.
*/
const applyOilEffect = (ctx, width, height, radius) => {
radius = Math.max(1, Math.floor(radius));
const srcData = ctx.getImageData(0, 0, width, height);
const dstData = ctx.createImageData(width, height);
const src = srcData.data;
const dst = dstData.data;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const sums = Array.from({ length: 4 }, () => [0, 0, 0]);
const squaredSums = Array.from({ length: 4 }, () => [0, 0, 0]);
const counts = [0, 0, 0, 0];
for (let j = -radius; j <= radius; j++) {
for (let i = -radius; i <= radius; i++) {
const pX = x + i;
const pY = y + j;
if (pX >= 0 && pX < width && pY >= 0 && pY < height) {
const offset = (pY * width + pX) * 4;
const r = src[offset], g = src[offset + 1], b = src[offset + 2];
let quadrant = -1;
if (i <= 0 && j <= 0) quadrant = 0; // Top-left
else if (i > 0 && j <= 0) quadrant = 1; // Top-right
else if (i <= 0 && j > 0) quadrant = 2; // Bottom-left
else if (i > 0 && j > 0) quadrant = 3; // Bottom-right
if (quadrant > -1) {
counts[quadrant]++;
sums[quadrant][0] += r;
sums[quadrant][1] += g;
sums[quadrant][2] += b;
squaredSums[quadrant][0] += r * r;
squaredSums[quadrant][1] += g * g;
squaredSums[quadrant][2] += b * b;
}
}
}
}
let minVariance = -1;
let bestQuadrant = 0;
const means = Array.from({ length: 4 }, () => [0, 0, 0]);
for (let q = 0; q < 4; q++) {
if (counts[q] > 0) {
means[q][0] = sums[q][0] / counts[q];
means[q][1] = sums[q][1] / counts[q];
means[q][2] = sums[q][2] / counts[q];
const varianceR = squaredSums[q][0] / counts[q] - means[q][0] * means[q][0];
const varianceG = squaredSums[q][1] / counts[q] - means[q][1] * means[q][1];
const varianceB = squaredSums[q][2] / counts[q] - means[q][2] * means[q][2];
const totalVariance = varianceR + varianceG + varianceB;
if (minVariance === -1 || totalVariance < minVariance) {
minVariance = totalVariance;
bestQuadrant = q;
}
}
}
const dstOffset = (y * width + x) * 4;
dst[dstOffset] = means[bestQuadrant][0];
dst[dstOffset + 1] = means[bestQuadrant][1];
dst[dstOffset + 2] = means[bestQuadrant][2];
dst[dstOffset + 3] = src[(y * width + x) * 4 + 3];
}
}
ctx.putImageData(dstData, 0, 0);
};
/**
* Applies a pencil sketch effect using grayscale, inversion, blur, and color-dodge blending.
* @param {CanvasRenderingContext2D} ctx - The canvas context.
* @param {number} width - The width of the canvas.
* @param {number} height - The height of the canvas.
* @param {number} intensity - Controls the blur radius for the effect.
*/
const applySketchEffect = (ctx, width, height, intensity) => {
const radius = Math.max(1, Math.floor(intensity));
const originalCanvas = ctx.canvas;
const tempCanvas = document.createElement('canvas');
tempCanvas.width = width;
tempCanvas.height = height;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.filter = `grayscale(1) invert(1) blur(${radius}px)`;
tempCtx.drawImage(originalCanvas, 0, 0, width, height);
ctx.filter = `grayscale(1)`;
ctx.drawImage(originalCanvas, 0, 0, width, height);
ctx.filter = 'none';
ctx.globalCompositeOperation = 'color-dodge';
ctx.drawImage(tempCanvas, 0, 0, width, height);
ctx.globalCompositeOperation = 'source-over';
};
/**
* Applies a pointillism effect by redrawing the image with colored dots.
* @param {CanvasRenderingContext2D} ctx - The canvas context.
* @param {number} width - The width of the canvas.
* @param {number} height - The height of the canvas.
* @param {number} dotSize - The diameter of the dots.
*/
const applyPointillismEffect = (ctx, width, height, dotSize) => {
dotSize = Math.max(2, Math.floor(dotSize));
const originalImageData = ctx.getImageData(0, 0, width, height);
const originalData = originalImageData.data;
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, width, height);
const radius = dotSize / 2;
for (let y = 0; y < height; y += dotSize) {
for (let x = 0; x < width; x += dotSize) {
const sampleX = Math.min(x + Math.floor(radius), width - 1);
const sampleY = Math.min(y + Math.floor(radius), height - 1);
const offset = (sampleY * width + sampleX) * 4;
const r = originalData[offset];
const g = originalData[offset + 1];
const b = originalData[offset + 2];
const a = originalData[offset + 3];
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${a / 255})`;
ctx.beginPath();
ctx.arc(x + radius, y + radius, radius, 0, Math.PI * 2);
ctx.fill();
}
}
};
/**
* Applies a pop art effect by posterizing the image with a given color palette.
* @param {CanvasRenderingContext2D} ctx - The canvas context.
* @param {number} width - The width of the canvas.
* @param {number} height - The height of the canvas.
* @param {string} colorsStr - A comma-separated string of hex colors.
*/
const applyPopArtEffect = (ctx, width, height, colorsStr) => {
const hexToRgb = (hex) => {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex.trim());
return result ? { r: parseInt(result[1], 16), g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : null;
};
const colors = colorsStr.split(',').map(hexToRgb).filter(c => c !== null);
if (colors.length === 0) {
['#003366', '#ffcc00'].forEach(c => colors.push(hexToRgb(c))); // Default palette
}
const numColors = colors.length;
const step = 256 / numColors;
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const r = data[i], g = data[i + 1], b = data[i + 2];
const brightness = 0.299 * r + 0.587 * g + 0.114 * b;
const colorIndex = Math.min(Math.floor(brightness / step), numColors - 1);
const newColor = colors[colorIndex];
data[i] = newColor.r;
data[i + 1] = newColor.g;
data[i + 2] = newColor.b;
}
ctx.putImageData(imageData, 0, 0);
};
// --- Route to the correct effect based on 'style' parameter ---
try {
switch (style.toLowerCase()) {
case 'oil':
applyOilEffect(ctx, canvas.width, canvas.height, intensity);
break;
case 'sketch':
applySketchEffect(ctx, canvas.width, canvas.height, intensity);
break;
case 'pointillism':
applyPointillismEffect(ctx, canvas.width, canvas.height, intensity);
break;
case 'popart':
applyPopArtEffect(ctx, canvas.width, canvas.height, colors);
break;
default:
console.warn(`Unknown style: '${style}'. Returning original image.`);
break;
}
} catch (e) {
console.error("Could not process image due to a security error (e.g., cross-origin taint).", e);
const errorDiv = document.createElement('div');
errorDiv.style.cssText = 'color: red; padding: 20px; border: 1px solid red; background: #ffeeee; font-family: sans-serif;';
errorDiv.textContent = 'Error: Could not process the image. This might be due to cross-origin restrictions.';
return errorDiv;
}
return canvas;
}
Apply Changes