Please bookmark this page to avoid losing your image tool!

Image Chalk Drawing Filter

(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,
    chalkColorStr = "255,255,250", // Off-white chalk
    boardColorStr = "40,40,60",    // Dark slate blue board
    edgeThreshold = 120,           // For Sobel magnitude (approx range 0-1442)
    chalkThickness = 1,            // 0: 1px lines, 1: 3x3 thickness, 2: 5x5 thickness
    noise = 0.1                    // 0-1, intensity of noise texture
) {
    const width = originalImg.naturalWidth || originalImg.width;
    const height = originalImg.naturalHeight || originalImg.height;

    if (width === 0 || height === 0) {
        console.error("Image has zero dimensions.");
        const emptyCanvas = document.createElement('canvas');
        emptyCanvas.width = 1; emptyCanvas.height = 1; // Return a minimal canvas
        return emptyCanvas;
    }

    const canvas = document.createElement('canvas');
    canvas.width = width;
    canvas.height = height;
    const ctx = canvas.getContext('2d', { willReadFrequently: true }); // Optimization hint
    
    try {
        ctx.drawImage(originalImg, 0, 0, width, height);
    } catch (e) {
        console.error("Error drawing image (possibly not loaded or invalid):", e);
        ctx.font = "16px Arial";
        ctx.fillStyle = "red";
        ctx.fillText("Error drawing image.", 10, Math.min(20, height - 5));
        return canvas;
    }
    
    let originalImageData;
    try {
        originalImageData = ctx.getImageData(0, 0, width, height);
    } catch (e) {
        console.error("Error getting ImageData (possibly tainted canvas due to cross-origin image):", e);
        ctx.clearRect(0,0,width,height); // Clear the potentially drawn image if data can't be read
        ctx.font = "16px Arial";
        ctx.fillStyle = "red";
        ctx.fillText("Error: Cannot process cross-origin image.", 10, Math.min(20, height - 5));
        return canvas;
    }
    
    const originalData = originalImageData.data;

    const parsedChalkColor = chalkColorStr.split(',').map(Number);
    const parsedBoardColor = boardColorStr.split(',').map(Number);

    // 1. Create grayscale version of the image
    const grayScaleData = new Uint8ClampedArray(width * height);
    for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
            const i = (y * width + x) * 4;
            const r = originalData[i];
            const g = originalData[i + 1];
            const b = originalData[i + 2];
            grayScaleData[y * width + x] = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
        }
    }

    // Sobel kernels
    const Kx = [
        [-1, 0, 1],
        [-2, 0, 2],
        [-1, 0, 1]
    ];
    const Ky = [
        [-1, -2, -1],
        [ 0,  0,  0],
        [ 1,  2,  1]
    ];

    // 2. Compute Sobel edge magnitudes
    const edgeMagnitudes = new Float32Array(width * height);
    for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
            let gx = 0;
            let gy = 0;
            for (let ky = -1; ky <= 1; ky++) {
                for (let kx = -1; kx <= 1; kx++) {
                    const cX = Math.max(0, Math.min(x + kx, width - 1));
                    const cY = Math.max(0, Math.min(y + ky, height - 1));
                    const pixelValue = grayScaleData[cY * width + cX];

                    gx += Kx[ky + 1][kx + 1] * pixelValue;
                    gy += Ky[ky + 1][kx + 1] * pixelValue;
                }
            }
            edgeMagnitudes[y * width + x] = Math.sqrt(gx * gx + gy * gy);
        }
    }
    
    // 3. Create binary edge mask from magnitudes
    let edgesMask = new Uint8ClampedArray(width * height);
    for (let i = 0; i < edgeMagnitudes.length; i++) {
        if (edgeMagnitudes[i] > edgeThreshold) {
            edgesMask[i] = 1;
        } else {
            edgesMask[i] = 0;
        }
    }

    // 4. Dilate edge mask if chalkThickness requires it
    // kernelRadius = 0 means 1x1 (no change beyond original pixel)
    // kernelRadius = 1 means 3x3 neighborhood etc.
    const kernelRadius = Math.max(0, Math.floor(chalkThickness)); 
    
    if (kernelRadius > 0) {
        const dilatedEdgesMask = new Uint8ClampedArray(width * height); // Initialize with 0s
        for (let y = 0; y < height; y++) {
            for (let x = 0; x < width; x++) {
                let isSet = 0;
                for (let dy = -kernelRadius; dy <= kernelRadius; dy++) {
                    for (let dx = -kernelRadius; dx <= kernelRadius; dx++) {
                        // Check original edgesMask at (x+dx, y+dy)
                        const currentX = x + dx; 
                        const currentY = y + dy;
                        if (currentX >= 0 && currentX < width && currentY >= 0 && currentY < height) {
                            if (edgesMask[currentY * width + currentX] === 1) {
                                isSet = 1;
                                break;
                            }
                        }
                    }
                    if (isSet) break;
                }
                dilatedEdgesMask[y * width + x] = isSet;
            }
        }
        edgesMask = dilatedEdgesMask; // Use the dilated mask
    }


    // 5. Create output image data and render final image
    const outputCanvas = document.createElement('canvas');
    outputCanvas.width = width;
    outputCanvas.height = height;
    const outputCtx = outputCanvas.getContext('2d');
    const outputImageData = outputCtx.createImageData(width, height);
    const outputData = outputImageData.data;

    // Max noise deviation (e.g., noise=0.1 -> noise effect range +/- 5)
    const noiseEffectStrength = noise * 50; 

    for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
            const i = (y * width + x) * 4;
            const maskVal = edgesMask[y * width + x];
            
            // Generate noise once per pixel, scale differently for chalk/board if needed
            const randomNoiseBase = (Math.random() - 0.5) * 2; // Range: -1 to 1

            if (maskVal === 1) { // Chalk pixel
                const chalkNoise = randomNoiseBase * noiseEffectStrength;
                outputData[i]     = Math.max(0, Math.min(255, parsedChalkColor[0] + chalkNoise));
                outputData[i + 1] = Math.max(0, Math.min(255, parsedChalkColor[1] + chalkNoise));
                outputData[i + 2] = Math.max(0, Math.min(255, parsedChalkColor[2] + chalkNoise));
                outputData[i + 3] = 255;
            } else { // Board pixel
                const boardNoise = randomNoiseBase * noiseEffectStrength * 0.5; // Board gets half noise
                outputData[i]     = Math.max(0, Math.min(255, parsedBoardColor[0] + boardNoise));
                outputData[i + 1] = Math.max(0, Math.min(255, parsedBoardColor[1] + boardNoise));
                outputData[i + 2] = Math.max(0, Math.min(255, parsedBoardColor[2] + boardNoise));
                outputData[i + 3] = 255;
            }
        }
    }

    outputCtx.putImageData(outputImageData, 0, 0);
    return outputCanvas;
}

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 Chalk Drawing Filter transforms your images into chalk-style drawings, mimicking the look of art drawn on a chalkboard. It allows users to customize the chalk and board colors, edge detection sensitivity, chalk thickness, and the level of noise for a more realistic effect. This tool is ideal for creating unique artistic renditions of photos for educational materials, graphic design projects, social media content, or just for fun. Users can experiment with different settings to achieve the desired artistic outcome.

Leave a Reply

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