Please bookmark this page to avoid losing your image tool!

Cinematic Mood 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,
    desaturationLevel = 0.3,     // Number: 0.0 to 1.0. 0 for no change, 1 for full grayscale.
    contrastLevel = 1.2,         // Number: 0.0 to N. 1.0 for no change.
    shadowsTintColor = "#003366",  // String: Hex color for shadows (e.g., deep blue/teal).
    highlightsTintColor = "#FFA500",// String: Hex color for highlights (e.g., warm orange/yellow).
    splitToneIntensity = 0.35    // Number: 0.0 to 1.0. Intensity of the split toning effect.
) {
    // Helper function to parse hex color string to an {r, g, b} object
    // Returns null if hex is invalid
    function hexToRgb(hex) {
        if (!hex || typeof hex !== 'string' || !hex.startsWith('#')) {
            return null;
        }
        
        let hexValue = hex.slice(1);
        
        if (hexValue.length === 3) { // Expand shorthand form (e.g., #03F to #0033FF)
            hexValue = hexValue[0] + hexValue[0] + hexValue[1] + hexValue[1] + hexValue[2] + hexValue[2];
        }
        
        if (hexValue.length !== 6 || !/^[0-9A-Fa-f]{6}$/.test(hexValue)) {
            return null; // Invalid hex value
        }

        const bigint = parseInt(hexValue, 16);
        const r = (bigint >> 16) & 255;
        const g = (bigint >> 8) & 255;
        const b = bigint & 255;
        return { r, g, b };
    }

    // Ensure the image is loaded
    if (!originalImg.complete || originalImg.naturalWidth === 0) {
        // If the image src is set and it's still loading, wait for it.
        // If src is not set or invalid, this might not resolve or reject as expected.
        // However, an `Image` object passed as a parameter usually implies it's intended to be valid.
        if (originalImg.src) {
            try {
                await new Promise((resolve, reject) => {
                    originalImg.onload = resolve;
                    originalImg.onerror = () => reject(new Error("Image failed to load. Check the image source or network."));
                });
            } catch (error) {
                console.error(error.message);
                const errorCanvas = document.createElement('canvas');
                errorCanvas.width = 300; errorCanvas.height = 60;
                const errCtx = errorCanvas.getContext('2d');
                errCtx.font = "12px Arial";
                errCtx.fillStyle = "red";
                errCtx.fillText(error.message, 10, 25);
                errCtx.fillText("Cannot process the image.", 10, 45);
                return errorCanvas;
            }
        }
    }
    
    // After attempting to load, check if dimensions are valid
    if (originalImg.naturalWidth === 0 || originalImg.naturalHeight === 0) {
        console.error("Image has zero dimensions or could not be loaded.");
        const errorCanvas = document.createElement('canvas');
        errorCanvas.width = 300; errorCanvas.height = 60;
        const errCtx = errorCanvas.getContext('2d');
        errCtx.font = "12px Arial";
        errCtx.fillStyle = "red";
        errCtx.fillText("Error: Image has invalid dimensions or failed to load.", 10, 25);
        errCtx.fillText("Cannot process the image.", 10, 45);
        return errorCanvas;
    }

    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');

    canvas.width = originalImg.naturalWidth;
    canvas.height = originalImg.naturalHeight;

    ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);

    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    const data = imageData.data;

    const shadowRgb = hexToRgb(shadowsTintColor);
    const highlightRgb = hexToRgb(highlightsTintColor);
    
    const epsilon = 1e-6; // For floating point comparisons

    for (let i = 0; i < data.length; i += 4) {
        let r_val = data[i];
        let g_val = data[i+1];
        let b_val = data[i+2];

        // 1. Desaturation
        // Only apply if desaturationLevel is meaningfully greater than 0
        if (desaturationLevel > epsilon) {
            const gray = 0.299 * r_val + 0.587 * g_val + 0.114 * b_val; // Luma-based grayscale
            r_val = r_val * (1 - desaturationLevel) + gray * desaturationLevel;
            g_val = g_val * (1 - desaturationLevel) + gray * desaturationLevel;
            b_val = b_val * (1 - desaturationLevel) + gray * desaturationLevel;
        }

        // 2. Contrast
        // Only apply if contrastLevel is meaningfully different from 1.0
        if (Math.abs(contrastLevel - 1.0) > epsilon) {
            // Apply contrast: f(v) = (v/255 - 0.5) * factor + 0.5
            // then scale back to 0-255
            r_val = ((r_val / 255 - 0.5) * contrastLevel + 0.5) * 255;
            g_val = ((g_val / 255 - 0.5) * contrastLevel + 0.5) * 255;
            b_val = ((b_val / 255 - 0.5) * contrastLevel + 0.5) * 255;
        }
        
        // Clamp values after desaturation & contrast, before split toning
        r_val = Math.max(0, Math.min(255, r_val));
        g_val = Math.max(0, Math.min(255, g_val));
        b_val = Math.max(0, Math.min(255, b_val));

        // 3. Split Toning
        // Only apply if intensity is meaningfully greater than 0
        if (splitToneIntensity > epsilon) {
            // Calculate luminance from the current (desaturated, contrasted) color
            const currentLuminance = (0.299 * r_val + 0.587 * g_val + 0.114 * b_val) / 255; // Normalized 0-1

            // Apply shadow tint
            if (shadowRgb) {
                const shadowMixFactor = (1 - currentLuminance) * splitToneIntensity;
                r_val = r_val * (1 - shadowMixFactor) + shadowRgb.r * shadowMixFactor;
                g_val = g_val * (1 - shadowMixFactor) + shadowRgb.g * shadowMixFactor;
                b_val = b_val * (1 - shadowMixFactor) + shadowRgb.b * shadowMixFactor;
            }

            // Apply highlight tint (to the already shadow-tinted color for cumulative effect)
            if (highlightRgb) {
                const highlightMixFactor = currentLuminance * splitToneIntensity;
                r_val = r_val * (1 - highlightMixFactor) + highlightRgb.r * highlightMixFactor;
                g_val = g_val * (1 - highlightMixFactor) + highlightRgb.g * highlightMixFactor;
                b_val = b_val * (1 - highlightMixFactor) + highlightRgb.b * highlightMixFactor;
            }
        }

        // Final clamp and assignment
        data[i]   = Math.max(0, Math.min(255, r_val));
        data[i+1] = Math.max(0, Math.min(255, g_val));
        data[i+2] = Math.max(0, Math.min(255, b_val));
        // data[i+3] is alpha, remains unchanged
    }

    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 Cinematic Mood Photo Filter is a web-based tool that allows users to enhance their photos with a cinematic look. It offers customizable settings to adjust desaturation, contrast, and color tints for shadows and highlights, providing a unique split toning effect. This tool is ideal for photographers and social media enthusiasts who wish to create visually appealing and atmospheric images for sharing online or for personal use.

Leave a Reply

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