Please bookmark this page to avoid losing your image tool!

Image Risograph Filter Application

(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, colorsStr = "FF5F00,0074D9", dotSize = 5, grainAmount = 15, offsetX = 1, offsetY = 1, blendMode = "multiply", backgroundColor = "#FFFFFF") {
    const imgWidth = originalImg.naturalWidth || originalImg.width;
    const imgHeight = originalImg.naturalHeight || originalImg.height;

    if (imgWidth === 0 || imgHeight === 0) {
        const emptyCanvas = document.createElement('canvas');
        emptyCanvas.width = 1;
        emptyCanvas.height = 1;
        const emptyCtx = emptyCanvas.getContext('2d');
        if (emptyCtx) {
             emptyCtx.fillStyle = backgroundColor;
             emptyCtx.fillRect(0, 0, 1, 1);
        }
        return emptyCanvas;
    }

    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    canvas.width = imgWidth;
    canvas.height = imgHeight;

    // Fill background on the main canvas
    ctx.fillStyle = backgroundColor;
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    const parsedColors = colorsStr.split(',')
        .map(c => c.trim())
        .filter(c => c.length > 0)
        .map(c => `#${c.replace('#', '')}`);

    if (parsedColors.length === 0) {
        // Fallback if colorsStr is empty or results in no valid colors
        parsedColors.push("#FF5F00");
        parsedColors.push("#0074D9");
    }

    // Create Grayscale version of the image for intensity mapping
    const grayscaleCanvas = document.createElement('canvas');
    grayscaleCanvas.width = canvas.width;
    grayscaleCanvas.height = canvas.height;
    // Add { willReadFrequently: true } for potential performance optimization
    const gsCtx = grayscaleCanvas.getContext('2d', { willReadFrequently: true });

    // Draw original image to grayscale canvas to get its pixel data including alpha
    gsCtx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);

    const imgDataForGrayscale = gsCtx.getImageData(0, 0, canvas.width, canvas.height);
    const gsPixels = imgDataForGrayscale.data;
    for (let i = 0; i < gsPixels.length; i += 4) {
        const r = gsPixels[i];
        const g = gsPixels[i + 1];
        const b = gsPixels[i + 2];
        // Standard luminance calculation
        const gray = 0.299 * r + 0.587 * g + 0.114 * b;
        gsPixels[i] = gsPixels[i + 1] = gsPixels[i + 2] = gray;
        // Alpha (gsPixels[i+3]) is preserved from original image
    }
    gsCtx.putImageData(imgDataForGrayscale, 0, 0);

    const actualDotSize = Math.max(1, Math.floor(dotSize)); // dotSize should be integer and >= 1

    parsedColors.forEach((color, layerIndex) => {
        const layerCanvas = document.createElement('canvas');
        layerCanvas.width = canvas.width;
        layerCanvas.height = canvas.height;
        const layerCtx = layerCanvas.getContext('2d');
        layerCtx.fillStyle = color;

        // Halftone effect for this layer
        for (let y = 0; y < canvas.height; y += actualDotSize) {
            for (let x = 0; x < canvas.width; x += actualDotSize) {
                // Adjust block size for edges of the image
                const blockWidth = Math.min(actualDotSize, canvas.width - x);
                const blockHeight = Math.min(actualDotSize, canvas.height - y);

                if (blockWidth <= 0 || blockHeight <= 0) continue;

                // Get average brightness from grayscaleCanvas for the current cell
                const blockImageData = gsCtx.getImageData(x, y, blockWidth, blockHeight);
                const blockPixels = blockImageData.data;
                let totalGray = 0;
                let numSignificantPixelsInBlock = 0; // Counts pixels that contribute to "ink"

                for (let k = 0; k < blockPixels.length; k += 4) {
                    // Consider pixel if it's not fully transparent (or mostly transparent)
                    if (blockPixels[k+3] > 32) { // Alpha threshold (0-255)
                        totalGray += blockPixels[k]; // R, G, B are all gray
                        numSignificantPixelsInBlock++;
                    }
                }

                if (numSignificantPixelsInBlock === 0) continue; // Skip if block is effectively transparent

                const avgGray = totalGray / numSignificantPixelsInBlock;

                // normalizedIntensity: 0 for black (darkest), 1 for white (lightest).
                const normalizedIntensity = avgGray / 255;

                // dotRadiusFactor: 1 for black (full dot), 0 for white (no dot).
                // This means darker areas of the original image get more ink.
                const dotRadiusFactor = 1.0 - normalizedIntensity;

                 // Define a minimum radius for a dot to be visible, relative to cell size
                const minRadiusThreshold = 0.1 * (actualDotSize / 2) ; // e.g. 10% of max radius
                const dotRadius = (actualDotSize / 2) * dotRadiusFactor;

                if (dotRadius > minRadiusThreshold) {
                    layerCtx.beginPath();
                    // Center dot in the middle of the potentially smaller block at edges
                    layerCtx.arc(x + blockWidth / 2, y + blockHeight / 2, Math.max(0, dotRadius), 0, 2 * Math.PI, false);
                    layerCtx.fill();
                }
            }
        }

        // Composite this layer onto the main canvas
        if (layerIndex > 0) {
            ctx.globalCompositeOperation = blendMode;
        } else {
            // First layer is drawn directly over the background
            ctx.globalCompositeOperation = 'source-over';
        }

        const currentOffsetX = layerIndex * offsetX;
        const currentOffsetY = layerIndex * offsetY;

        ctx.drawImage(layerCanvas, currentOffsetX, currentOffsetY);
    });

    // Reset composite operation for grain application or other subsequent drawing
    ctx.globalCompositeOperation = 'source-over';

    // Add Grain (if grainAmount > 0)
    const absGrainAmount = Math.abs(grainAmount);
    if (absGrainAmount > 0 && absGrainAmount <= 255) {
        const finalImgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
        const finalPixels = finalImgData.data;

        for (let i = 0; i < finalPixels.length; i += 4) {
            // Only apply grain to pixels that are not fully transparent
            if (finalPixels[i+3] === 0) continue;

            // Add monochrome noise
            const noise = (Math.random() - 0.5) * absGrainAmount;
            finalPixels[i]   = Math.max(0, Math.min(255, finalPixels[i] + noise));
            finalPixels[i+1] = Math.max(0, Math.min(255, finalPixels[i+1] + noise));
            finalPixels[i+2] = Math.max(0, Math.min(255, finalPixels[i+2] + noise));
        }
        ctx.putImageData(finalImgData, 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 Image Risograph Filter Application allows users to apply a unique risograph-style effect to their images. By simulating the halftone print effect, users can select a set of colors and customize dot size and grain amount to create visually striking artwork. This tool is ideal for graphic designers, artists, and anyone looking to add a vintage print aesthetic to their digital images. It’s particularly useful for enhancing photos for social media, creating promotional materials, or producing art prints with a distinctive look.

Leave a Reply

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