Please bookmark this page to avoid losing your image tool!

Photo Translation Variant Painter

(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, 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;
}

Free Image Tool Creator

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

Description

Photo Translation Variant Painter is an online tool that allows users to apply various artistic effects to their images. With options including oil painting, pencil sketch, pointillism, and pop art styles, users can transform their photos into unique artworks. This tool is ideal for artists, designers, or anyone looking to creatively enhance their images for social media, gifts, or personal projects.

Leave a Reply

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