You can edit the below JavaScript code to customize the image tool.
Apply Changes
/**
* Converts an image to an oil painting style artwork.
* This effect is achieved by using a Kuwahara-like filter, which reduces noise
* and detail while preserving edges, mimicking the appearance of brush strokes.
* An optional color quantization step is also applied to create larger, flatter color areas.
*
* @param {HTMLImageElement} originalImg The original image element to process.
* @param {number} [radius=4] The radius of the brush stroke effect. Larger values create more abstract, larger strokes.
* @param {number} [intensity=50] The number of color levels per channel. Lower values create a more posterized, stylized look with fewer colors. A value of 256 effectively disables this step.
* @returns {HTMLCanvasElement} A new canvas element displaying the oil painted image.
*/
function processImage(originalImg, radius = 4, intensity = 50) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const width = originalImg.naturalWidth;
const height = originalImg.naturalHeight;
canvas.width = width;
canvas.height = height;
ctx.drawImage(originalImg, 0, 0);
const srcImageData = ctx.getImageData(0, 0, width, height);
const srcData = srcImageData.data;
// Create a copy for processing to have a non-quantized source for the filter
const quantizedData = new Uint8ClampedArray(srcData);
// 1. Pre-processing: Color Quantization based on intensity
// A higher intensity means more color levels.
// Clamp intensity to a reasonable range [2, 256].
const numLevels = Math.max(2, Math.min(256, Math.floor(intensity)));
if (numLevels < 256) { // No need to quantize if 256 levels are requested
const step = 255 / (numLevels - 1);
for (let i = 0; i < quantizedData.length; i += 4) {
quantizedData[i] = Math.round(quantizedData[i] / step) * step; // R
quantizedData[i + 1] = Math.round(quantizedData[i + 1] / step) * step; // G
quantizedData[i + 2] = Math.round(quantizedData[i + 2] / step) * step; // B
}
}
// 2. Main Filter (Kuwahara-like effect)
const dstImageData = ctx.createImageData(width, height);
const dstData = dstImageData.data;
const r = Math.floor(radius);
const getIndex = (x, y) => (y * width + x) * 4;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = getIndex(x, y);
// Quadrant data format: [sumR, sumG, sumB, sumLuminanceSq, count]
const quadrants = [
[0, 0, 0, 0, 0], // Top-left
[0, 0, 0, 0, 0], // Top-right
[0, 0, 0, 0, 0], // Bottom-left
[0, 0, 0, 0, 0] // Bottom-right
];
const sumsLuminance = [0, 0, 0, 0];
// Loop through the neighborhood window
for (let j = -r; j <= r; j++) {
for (let k = -r; k <= r; k++) {
const neighborY = y + j;
const neighborX = x + k;
// Check image bounds
if (neighborY >= 0 && neighborY < height && neighborX >= 0 && neighborX < width) {
const neighborIndex = getIndex(neighborX, neighborY);
const rVal = quantizedData[neighborIndex];
const gVal = quantizedData[neighborIndex + 1];
const bVal = quantizedData[neighborIndex + 2];
const luminance = 0.299 * rVal + 0.587 * gVal + 0.114 * bVal;
// Determine which quadrant the neighbor pixel falls into
let quadrantIndex;
if (j <= 0 && k <= 0) quadrantIndex = 0; // Top-left
else if (j <= 0 && k > 0) quadrantIndex = 1; // Top-right
else if (j > 0 && k <= 0) quadrantIndex = 2; // Bottom-left
else quadrantIndex = 3; // Bottom-right
quadrants[quadrantIndex][0] += rVal;
quadrants[quadrantIndex][1] += gVal;
quadrants[quadrantIndex][2] += bVal;
quadrants[quadrantIndex][3] += luminance * luminance;
quadrants[quadrantIndex][4]++;
sumsLuminance[quadrantIndex] += luminance;
}
}
}
// Calculate variance and find the quadrant with the minimum variance
let minVariance = -1;
let finalR = 0, finalG = 0, finalB = 0;
for (let q = 0; q < 4; q++) {
const count = quadrants[q][4];
if (count === 0) continue;
const meanR = quadrants[q][0] / count;
const meanG = quadrants[q][1] / count;
const meanB = quadrants[q][2] / count;
const meanLuminance = sumsLuminance[q] / count;
// Variance = E[X^2] - (E[X])^2
const variance = (quadrants[q][3] / count) - (meanLuminance * meanLuminance);
if (minVariance === -1 || variance < minVariance) {
minVariance = variance;
finalR = meanR;
finalG = meanG;
finalB = meanB;
}
}
// Set the destination pixel color to the mean color of the smoothest quadrant
dstData[i] = finalR;
dstData[i + 1] = finalG;
dstData[i + 2] = finalB;
dstData[i + 3] = 255; // Alpha
}
}
// 3. Put the processed data back onto the canvas
ctx.putImageData(dstImageData, 0, 0);
return canvas;
}
Apply Changes