Please bookmark this page to avoid losing your image tool!

Magic Paint Image Tool

(Free & Supports Bulk Upload)

Drag & drop your images here or

The result will appear here...
You can edit the below JavaScript code to customize the image tool.
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;
}

Free Image Tool Creator

Can't find the image tool you're looking for?
Create one based on your own needs now!

Description

The Magic Paint Image Tool transforms standard photographs into artistic, painterly images by applying a Kuwahara filter effect. The tool can enhance color saturation, adjust brush stroke sizes, and apply multiple processing passes to create a smooth, stylized appearance while preserving important edges. This is ideal for users looking to create digital art, stylized profile pictures, or unique textures for creative design projects.

Leave a Reply

Your email address will not be published. Required fields are marked *