You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, brushSize = 6, saturation = 1.2, passes = 1, sharpen = 0.5) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Scale down image to reasonable dimensions for processing speed
const MAX_SIZE = 1200;
let width = originalImg.width;
let height = originalImg.height;
if (width > MAX_SIZE || height > MAX_SIZE) {
const ratio = Math.min(MAX_SIZE / width, MAX_SIZE / height);
width = Math.round(width * ratio);
height = Math.round(height * ratio);
}
canvas.width = width;
canvas.height = height;
ctx.drawImage(originalImg, 0, 0, width, height);
let imgData = ctx.getImageData(0, 0, width, height);
let data = imgData.data;
// Saturation pass - enhances colors before painting effect
const sat = Number(saturation);
if (sat !== 1) {
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i+1];
const b = data[i+2];
const l = 0.299 * r + 0.587 * g + 0.114 * b;
data[i] = Math.min(255, Math.max(0, l + (r - l) * sat));
data[i+1] = Math.min(255, Math.max(0, l + (g - l) * sat));
data[i+2] = Math.min(255, Math.max(0, l + (b - l) * sat));
}
}
const radius = Math.max(1, Math.min(20, Number(brushSize)));
const passesCount = Math.max(1, Math.min(3, Math.round(Number(passes))));
const size = width * height;
// We use Float64Array to hold potentially very large sums of squared luminance
const sumR = new Float64Array(size);
const sumG = new Float64Array(size);
const sumB = new Float64Array(size);
const sumL = new Float64Array(size);
const sumL2 = new Float64Array(size);
// Helper to get sum of given array within bounding box (x1, y1) to (x2, y2)
const getSum = (arr, x1, y1, x2, y2) => {
x1 = Math.max(0, x1);
y1 = Math.max(0, y1);
x2 = Math.min(width - 1, x2);
y2 = Math.min(height - 1, y2);
let a = 0, b = 0, c = 0, d = arr[y2 * width + x2];
if (x1 > 0 && y1 > 0) a = arr[(y1 - 1) * width + (x1 - 1)];
if (x1 > 0) b = arr[y2 * width + (x1 - 1)];
if (y1 > 0) c = arr[(y1 - 1) * width + x2];
return d - b - c + a;
};
// Helper to calculate area of overlapping regions dynamically
const getArea = (x1, y1, x2, y2) => {
x1 = Math.max(0, x1);
y1 = Math.max(0, y1);
x2 = Math.min(width - 1, x2);
y2 = Math.min(height - 1, y2);
return (x2 - x1 + 1) * (y2 - y1 + 1);
};
// Magic Paint (Kuwahara filter) passes
for (let p = 0; p < passesCount; p++) {
// Build summed area tables (Integral Images) to make region calculations an O(1) operation
for (let y = 0; y < height; y++) {
let rowSumR = 0, rowSumG = 0, rowSumB = 0, rowSumL = 0, rowSumL2 = 0;
for (let x = 0; x < width; x++) {
const idx = (y * width + x) * 4;
const idx1d = y * width + x;
const r = data[idx], g = data[idx+1], b = data[idx+2];
const l = 0.299 * r + 0.587 * g + 0.114 * b;
rowSumR += r;
rowSumG += g;
rowSumB += b;
rowSumL += l;
rowSumL2 += l * l;
if (y === 0) {
sumR[idx1d] = rowSumR;
sumG[idx1d] = rowSumG;
sumB[idx1d] = rowSumB;
sumL[idx1d] = rowSumL;
sumL2[idx1d] = rowSumL2;
} else {
const prevIdx = (y - 1) * width + x;
sumR[idx1d] = sumR[prevIdx] + rowSumR;
sumG[idx1d] = sumG[prevIdx] + rowSumG;
sumB[idx1d] = sumB[prevIdx] + rowSumB;
sumL[idx1d] = sumL[prevIdx] + rowSumL;
sumL2[idx1d] = sumL2[prevIdx] + rowSumL2;
}
}
}
const outData = ctx.createImageData(width, height);
const dst = outData.data;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let minVar = Infinity;
let bestR = 0, bestG = 0, bestB = 0;
// Construct the 4 overlapping regions surrounding the pixel
const regions = [
[x - radius, y - radius, x, y], // Top-Left
[x, y - radius, x + radius, y], // Top-Right
[x - radius, y, x, y + radius], // Bottom-Left
[x, y, x + radius, y + radius] // Bottom-Right
];
for (let i = 0; i < 4; i++) {
const [x1, y1, x2, y2] = regions[i];
const area = getArea(x1, y1, x2, y2);
const sL = getSum(sumL, x1, y1, x2, y2);
const sL2 = getSum(sumL2, x1, y1, x2, y2);
const meanL = sL / area;
const variance = (sL2 / area) - (meanL * meanL);
// Select the region with smoothest gradient (lowest luminance variance)
if (variance < minVar) {
minVar = variance;
bestR = getSum(sumR, x1, y1, x2, y2) / area;
bestG = getSum(sumG, x1, y1, x2, y2) / area;
bestB = getSum(sumB, x1, y1, x2, y2) / area;
}
}
const outIdx = (y * width + x) * 4;
dst[outIdx] = bestR;
dst[outIdx+1] = bestG;
dst[outIdx+2] = bestB;
dst[outIdx+3] = data[outIdx+3]; // Preserve original image alpha channel
}
}
data = dst;
imgData = outData;
}
// Edge enhancement post-processing to make "paint strokes" look authentic and thicker
const sharpAmount = Number(sharpen);
if (sharpAmount > 0) {
const outData = ctx.createImageData(width, height);
const dst = outData.data;
const src = imgData.data;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const idx = (y * width + x) * 4;
// Edge cases: maintain original borders to escape complex bounds logic
if (y === 0 || y === height - 1 || x === 0 || x === width - 1) {
dst[idx] = src[idx];
dst[idx+1] = src[idx+1];
dst[idx+2] = src[idx+2];
dst[idx+3] = src[idx+3];
continue;
}
const top = ((y - 1) * width + x) * 4;
const bot = ((y + 1) * width + x) * 4;
const left = (y * width + (x - 1)) * 4;
const right = (y * width + (x + 1)) * 4;
// Unsharp masking calculation matrix
for (let c = 0; c < 3; c++) {
let val = src[idx+c] * (1 + 4 * sharpAmount)
- sharpAmount * (src[top+c] + src[bot+c] + src[left+c] + src[right+c]);
dst[idx+c] = Math.min(255, Math.max(0, val));
}
dst[idx+3] = src[idx+3];
}
}
imgData = outData;
}
ctx.putImageData(imgData, 0, 0);
return canvas;
}
Apply Changes