Please bookmark this page to avoid losing your image tool!

Image VHS Photo 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.
async function processImage(originalImg, scanlineIntensity = 0.2, noiseAmount = 20, chromaticAberrationOffset = 2, distortionFrequency = 0.05, distortionAmplitude = 1, desaturation = 0.1) {
    // Ensure parameters are numbers as they might come from string sources (e.g., HTML attributes)
    scanlineIntensity = Number(scanlineIntensity);
    noiseAmount = Number(noiseAmount);
    chromaticAberrationOffset = Number(chromaticAberrationOffset);
    distortionFrequency = Number(distortionFrequency);
    distortionAmplitude = Number(distortionAmplitude);
    desaturation = Number(desaturation);

    const canvas = document.createElement('canvas');
    // Get context early for error drawing if needed.
    // willReadFrequently hint can optimize getImageData/putImageData on some browsers.
    const ctx = canvas.getContext('2d', { willReadFrequently: true });

    // Helper function to draw error messages on the canvas
    function drawError(message, detail = "") {
        const requestedWidth = originalImg.width || 0; // Attempt to use requested width for canvas size
        const requestedHeight = originalImg.height || 0;
        canvas.width = Math.max(200, requestedWidth); 
        canvas.height = Math.max(100, requestedHeight);
        
        ctx.fillStyle = '#DDDDDD'; // Light gray background
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        ctx.fillStyle = 'red';
        ctx.textAlign = 'center';
        
        const V_CENTER = canvas.height / 2;
        
        ctx.font = 'bold 14px sans-serif';
        ctx.fillText(message, canvas.width / 2, V_CENTER - (detail ? 10 : 0) );
        
        if (detail) {
            ctx.font = '12px sans-serif';
            // Limit detail length to prevent overflow
            ctx.fillText(detail.substring(0,100), canvas.width / 2, V_CENTER + 10);
        }
        console.error(message + (detail ? " - " + detail : ""));
        return canvas;
    }

    // Ensure image is loaded. originalImg should be an HTMLImageElement.
    // It might not be loaded if src was just set, or if it's an empty Image object.
    if (!originalImg.complete || originalImg.naturalWidth === 0) {
        // Check if there's a source attribute that could be loaded.
        // originalImg.src can be the full URL or "" if not set/invalid.
        // originalImg.currentSrc is the actual URL of the loaded image.
        const hasValidSrc = originalImg.src || (originalImg.currentSrc && originalImg.currentSrc !== window.location.href);

        if (hasValidSrc) {
            try {
                await new Promise((resolve, reject) => {
                    // If already complete and has dimensions (e.g. SVG loaded but naturalWidth was 0 initially)
                    if (originalImg.complete && originalImg.naturalWidth > 0 && originalImg.naturalHeight > 0) {
                        resolve();
                        return;
                    }

                    const existingOnload = originalImg.onload;
                    originalImg.onload = function(...args) {
                        if (typeof existingOnload === 'function') {
                            existingOnload.apply(originalImg, args);
                        }
                        resolve();
                    };

                    const existingOnerror = originalImg.onerror;
                    originalImg.onerror = function(...args) {
                         if (typeof existingOnerror === 'function') {
                            existingOnerror.apply(originalImg, args);
                        }
                        let errMsg = "Image failed to load.";
                        // Attempt to get a more specific error message
                        if (args[0] && typeof args[0] === 'object' && args[0].message) {
                             errMsg = args[0].message; // ErrorEvent
                        } else if (typeof args[0] === 'string') {
                             errMsg = args[0]; // Simple string error
                        }
                        reject(new Error(errMsg));
                    };
                    
                    // This handles cases where the image is already in a 'complete' but broken state (e.g. invalid src)
                    // and onload/onerror might not fire again for an already processed src.
                    if (originalImg.complete && (originalImg.naturalWidth === 0 || originalImg.naturalHeight === 0)) {
                         reject(new Error("Image is 'complete' but has zero dimensions, possibly broken or invalid."));
                    }
                });
            } catch (error) {
                return drawError('Error loading image.', error.message);
            }
        } else {
            return drawError('Image has no valid source attribute.');
        }
    }
    
    const width = originalImg.naturalWidth;
    const height = originalImg.naturalHeight;

    // Final check for valid dimensions after loading attempts
    if (width === 0 || height === 0) {
        return drawError('Image loaded but has zero dimensions.');
    }

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

    try {
        ctx.drawImage(originalImg, 0, 0, width, height);
    } catch (e) {
        // This error can occur if 'originalImg' is not a valid image source for 'drawImage'
        // (e.g. a broken image or an unsupported format that slipped through checks)
        return drawError('Error drawing image to canvas.', e.message);
    }
    
    let imageData;
    try {
        imageData = ctx.getImageData(0, 0, width, height);
    } catch (e) {
        // This usually happens due to tainted canvas (e.g., drawing a cross-origin image without CORS)
        const detailMsg = e.message + ( (e.name === "SecurityError") ? ' (Ensure image is from the same origin or has CORS headers enabled for cross-origin use).' : '' );
        return drawError('Cannot process image: Canvas operation restricted.', detailMsg);
    }

    const originalData = Uint8ClampedArray.from(imageData.data); // Keep a clean copy for sampling
    const outputData = imageData.data; // This is a reference, modifying it modifies imageData

    // Helper function to get a single color channel value from the original image data.
    // It handles boundary conditions by clamping coordinates to the image dimensions.
    function getPixelChannel(data, x, y, w, h, channel) {
        // Round coordinates to nearest pixel, then clamp to be within image bounds
        const clampedX = Math.max(0, Math.min(Math.round(x), w - 1));
        const clampedY = Math.max(0, Math.min(Math.round(y), h - 1));
        return data[(clampedY * w + clampedX) * 4 + channel]; // R=0, G=1, B=2, A=3
    }

    for (let y = 0; y < height; y++) {
        // Calculate horizontal distortion (wobble) for the current scanline
        // The sine wave creates a smooth, wave-like distortion.
        const yDistortion = distortionAmplitude * Math.sin(y * distortionFrequency);

        for (let x = 0; x < width; x++) {
            const outputIdx = (y * width + x) * 4; // Index for the current pixel in outputData

            // Determine the source X coordinate in the original image, applying the y-axis distortion
            const currentSourceX = x - yDistortion;

            // Chromatic Aberration: Sample R, G, B channels from slightly offset horizontal positions
            let r = getPixelChannel(originalData, currentSourceX - chromaticAberrationOffset, y, width, height, 0); // Red
            let g = getPixelChannel(originalData, currentSourceX, y, width, height, 1);                           // Green
            let b = getPixelChannel(originalData, currentSourceX + chromaticAberrationOffset, y, width, height, 2); // Blue
            // Alpha is assumed to be fully opaque for VHS effect. If transparency is needed:
            // let a = getPixelChannel(originalData, currentSourceX, y, width, height, 3); 


            // Desaturation: Reduce color intensity
            if (desaturation > 0) {
                const gray = 0.299 * r + 0.587 * g + 0.114 * b; // Standard NTSC luma coefficients
                const satFactor = Math.max(0, Math.min(1, desaturation)); // Clamp desaturation factor
                r = r * (1 - satFactor) + gray * satFactor;
                g = g * (1 - satFactor) + gray * satFactor;
                b = b * (1 - satFactor) + gray * satFactor;
            }

            // Noise: Add random intensity variations to pixel color components
            if (noiseAmount > 0) {
                // Generates noise ranging from -noiseAmount/2 to +noiseAmount/2
                const noiseVal = (Math.random() - 0.5) * noiseAmount; 
                r += noiseVal;
                g += noiseVal;
                b += noiseVal;
            }

            // Scanlines: Darken alternating lines to simulate CRT display
            if (scanlineIntensity > 0 && y % 2 !== 0) { // Apply to odd rows (or even, personal preference)
                const factor = Math.max(0, 1 - Math.min(1, scanlineIntensity)); // Clamp intensity effect
                r *= factor;
                g *= factor;
                b *= factor;
            }
            
            // Write processed pixel data to the output buffer.
            // Uint8ClampedArray automatically clamps values to the 0-255 range.
            outputData[outputIdx]     = r;
            outputData[outputIdx + 1] = g;
            outputData[outputIdx + 2] = b;
            outputData[outputIdx + 3] = 255; // Alpha channel (fully opaque)
        }
    }

    // Draw the modified pixel data back onto the canvas
    ctx.putImageData(imageData, 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 VHS Photo Filter is a web-based tool designed to apply a vintage VHS effect to your images. It simulates the nostalgic look of old video tapes by incorporating various adjustments, such as scanline effects, chromatic aberration, and noise. Users can customize parameters like scanline intensity, noise levels, and color desaturation to achieve their desired aesthetic. This tool is ideal for creative projects, social media posts, or any situation where a retro aesthetic is desired, allowing users to transform digital images into visually appealing, nostalgic representations.

Leave a Reply

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