Please bookmark this page to avoid losing your image tool!

Image Impasto Filter Effect 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.
function processImage(originalImg, brushRadius = 8, lightAngleDeg = 135, lightIntensity = 0.3) {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    
    // Ensure original image has valid dimensions
    if (originalImg.naturalWidth === 0 || originalImg.naturalHeight === 0) {
        console.error("Original image not loaded or has no dimensions.");
        // Create a dummy canvas to avoid errors if originalImg is broken
        canvas.width = 1;
        canvas.height = 1;
        return canvas;
    }

    canvas.width = originalImg.width;
    canvas.height = originalImg.height;

    // Create a source canvas to get pixel data from the original image
    // Using willReadFrequently for potential performance optimization if getImageData is called often
    const sourceCanvas = document.createElement('canvas');
    sourceCanvas.width = originalImg.width;
    sourceCanvas.height = originalImg.height;
    const sourceCtx = sourceCanvas.getContext('2d', { willReadFrequently: true });
    sourceCtx.drawImage(originalImg, 0, 0);

    // Light direction calculation
    const lightAngleRad = parseFloat(lightAngleDeg) * Math.PI / 180;
    // Standard angle: 0 is right, 90 is up. Canvas Y is inverted.
    const lightX = Math.cos(lightAngleRad);
    const lightY = -Math.sin(lightAngleRad); 

    // Normalize LightIntensity to be between 0 and 1
    const normalizedLightIntensity = Math.max(0, Math.min(1, parseFloat(lightIntensity)));

    // Map normalizedLightIntensity to practical ranges for effects
    const colorModStrength = normalizedLightIntensity * 0.45; // Modulates color by +-0% up to +-45%
    const displacementBase = parseFloat(brushRadius) * normalizedLightIntensity * 0.3; // Max displacement 30% of radius

    const R = parseFloat(brushRadius);
    const jitterRange = R * 0.5; // Jitter for stroke placement to make it more organic
    const step = Math.max(1, Math.floor(R * 0.65));  // Step for grid, ensures good overlap for coverage

    // Loop over a grid covering the canvas
    // Extend loop slightly beyond canvas dimensions to ensure edges are well covered by strokes
    for (let y = -R; y < canvas.height + R; y += step) {
        for (let x = -R; x < canvas.width + R; x += step) {
            // Add jitter to stroke center for a more natural, less grid-like appearance
            const jitterX = (Math.random() - 0.5) * jitterRange;
            const jitterY = (Math.random() - 0.5) * jitterRange;
            const centerX = x + jitterX;
            const centerY = y + jitterY;

            // Get color from original image at the (possibly jittered) stroke center
            // Clamp coordinates to be within the source image bounds
            const imgX = Math.max(0, Math.min(originalImg.width - 1, Math.floor(centerX)));
            const imgY = Math.max(0, Math.min(originalImg.height - 1, Math.floor(centerY)));
            
            const pixelData = sourceCtx.getImageData(imgX, imgY, 1, 1).data;
            const r = pixelData[0];
            const g = pixelData[1];
            const b = pixelData[2];
            const a = pixelData[3];

            // Skip fully or mostly transparent areas to avoid painting strokes for nothing
            if (a < 32) { // Alpha threshold (0-255), e.g., ignore if less than ~12% opaque
                continue;
            }

            // 1. Draw the shadow part of the stroke
            const sr = Math.max(0, Math.round(r * (1 - colorModStrength)));
            const sg = Math.max(0, Math.round(g * (1 - colorModStrength)));
            const sb = Math.max(0, Math.round(b * (1 - colorModStrength)));
            ctx.fillStyle = `rgb(${sr},${sg},${sb})`;
            ctx.beginPath();
            // Shadow is shifted away from the light source relative to the main stroke
            ctx.arc(centerX - lightX * displacementBase, 
                      centerY - lightY * displacementBase, 
                      R, 0, 2 * Math.PI);
            ctx.fill();

            // 2. Draw the main paint color part of the stroke
            ctx.fillStyle = `rgb(${r},${g},${b})`;
            ctx.beginPath();
            ctx.arc(centerX, centerY, R, 0, 2 * Math.PI);
            ctx.fill();
            
            // 3. Draw the highlight part of the stroke
            // Highlights can be slightly more intense
            const hr = Math.min(255, Math.round(r * (1 + colorModStrength * 1.1))); 
            const hg = Math.min(255, Math.round(g * (1 + colorModStrength * 1.1)));
            const hb = Math.min(255, Math.round(b * (1 + colorModStrength * 1.1)));
            ctx.fillStyle = `rgb(${hr},${hg},${hb})`;
            ctx.beginPath();
            // Highlight is smaller and shifted towards the light source
            // Use a slightly smaller displacement and radius for the highlight
            ctx.arc(centerX + lightX * (displacementBase * 0.7), 
                      centerY + lightY * (displacementBase * 0.7), 
                      R * 0.75, // Highlight radius is smaller
                      0, 2 * Math.PI);
            ctx.fill();
        }
    }
    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 Image Impasto Filter Effect Tool allows users to apply an artistic impasto effect to images, simulating the appearance of thick paint strokes. This tool can enhance images by adding texture and depth, making them appear more dynamic and expressive. Ideal for artists, graphic designers, or anyone looking to create unique visual styles, the tool permits customization through parameters such as brush radius, light angle, and light intensity, enabling a wide range of creative outcomes.

Leave a Reply

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