Please bookmark this page to avoid losing your image tool!

Image Split Tone 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, shadowColorStr = "#000080", highlightColorStr = "#FFFFE0", amount = 1.0) {
    
    /**
     * Parses a color string (e.g., hex, rgb, color name) into an [R, G, B] array.
     * @param {string} colorStr The color string to parse.
     * @returns {Array<number>|null} An array [R, G, B] or null if parsing fails.
     */
    function _parseColorInternal(colorStr) {
        if (!colorStr || typeof colorStr !== 'string') {
            return null;
        }

        // Create a temporary 2D context (doesn't need a visible canvas element)
        const ctx = document.createElement('canvas').getContext('2d');
        if (!ctx) { 
            // This should theoretically not happen in a standard browser environment.
            console.error("Failed to create 2D context for color parsing.");
            return null;
        }

        // Assign the color string to fillStyle. The browser will parse and canonicalize it.
        // First, set to a known distinct value to ensure proper parsing detection.
        ctx.fillStyle = 'rgba(0,0,0,1)'; // Opaque black, unlikely to be accidentally matched.
        ctx.fillStyle = colorStr;        // Attempt to parse the user's color string.
        
        const canonicalColor = ctx.fillStyle; // Read back the canonical color string (e.g., "#rrggbb").

        // Modern browsers convert valid colors to hex (#RRGGBB or #RRGGBBAA) or rgba().
        // We are interested in #RRGGBB format.
        // Example: "red" becomes "#ff0000", "rgb(0,255,0)" becomes "#00ff00".
        // If `colorStr` is invalid (e.g., "notAColor"), `fillStyle` often defaults to "#000000" (black).
        
        // Check if the canonical form is a hex color.
        if (canonicalColor.startsWith('#') && (canonicalColor.length === 7 || canonicalColor.length === 9)) {
            // If an invalid color string was provided and it defaulted to black,
            // this parser will return [0,0,0]. This is often an acceptable implicit fallback.
            const r = parseInt(canonicalColor.substring(1, 3), 16);
            const g = parseInt(canonicalColor.substring(3, 5), 16);
            const b = parseInt(canonicalColor.substring(5, 7), 16);
            
            if (!isNaN(r) && !isNaN(g) && !isNaN(b)) {
                 return [r, g, b];
            }
        }
        // Potentially handle rgba() string if browser returns that, though hex is more common for fillStyle.
        if (canonicalColor.startsWith('rgb')) { // e.g. rgb(0, 0, 0) or rgba(0, 0, 0, 1)
            const parts = canonicalColor.match(/(\d+(\.\d+)?)/g);
            if (parts && parts.length >= 3) {
                const r = parseInt(parts[0]);
                const g = parseInt(parts[1]);
                const b = parseInt(parts[2]);
                if (!isNaN(r) && !isNaN(g) && !isNaN(b)) {
                    return [r,g,b];
                }
            }
        }

        return null; // Parsing failed or color string was not recognized.
    }

    // Default RGB values (used if parsing fails or for clarity)
    const defaultShadowRGB = [0, 0, 128];    // Navy Blue
    const defaultHighlightRGB = [255, 255, 224]; // Light Yellow

    // Parse shadow color
    let [sR, sG, sB] = defaultShadowRGB;
    const parsedShadow = _parseColorInternal(shadowColorStr);
    if (parsedShadow) {
        [sR, sG, sB] = parsedShadow;
    } else if (shadowColorStr && shadowColorStr !== "") { // Warn only if a non-empty string failed
        console.warn(`Invalid shadowColor: "${shadowColorStr}". Using default: Navy Blue.`);
    }

    // Parse highlight color
    let [hR, hG, hB] = defaultHighlightRGB;
    const parsedHighlight = _parseColorInternal(highlightColorStr);
    if (parsedHighlight) {
        [hR, hG, hB] = parsedHighlight;
    } else if (highlightColorStr && highlightColorStr !== "") { // Warn only if a non-empty string failed
        console.warn(`Invalid highlightColor: "${highlightColorStr}". Using default: Light Yellow.`);
    }
    
    // Validate and sanitize the 'amount' parameter
    let numAmount = Number(amount);
    if (isNaN(numAmount)) {
        console.warn(`Invalid amount: "${amount}". Using default 1.0.`);
        numAmount = 1.0; 
    }
    numAmount = Math.max(0, Math.min(1, numAmount)); // Clamp amount to [0, 1] range

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

    // Get image dimensions. naturalWidth/Height for intrinsic size, fallback to width/height.
    const imgWidth = originalImg.naturalWidth || originalImg.width;
    const imgHeight = originalImg.naturalHeight || originalImg.height;

    canvas.width = imgWidth;
    canvas.height = imgHeight;
    
    // Handle cases where the image might not be loaded or has no dimensions.
    if (imgWidth === 0 || imgHeight === 0) {
        // console.warn("Image has zero width or height. Returning empty canvas.");
        return canvas; // Return empty (but correctly sized) canvas.
    }

    ctx.drawImage(originalImg, 0, 0, imgWidth, imgHeight);
    
    // Try-catch for getImageData, as it can throw security errors for cross-origin images without CORS.
    let imageData;
    try {
        imageData = ctx.getImageData(0, 0, imgWidth, imgHeight);
    } catch (e) {
        console.error("Failed to get image data, possibly due to CORS policy. Returning original image drawn on canvas.", e);
        // For cross-origin issues, the canvas is tainted. We can't process pixels.
        // Return the canvas with the image drawn, but not processed.
        return canvas;
    }
    
    const data = imageData.data;
    const originalWeight = 1.0 - numAmount; // Weight of the original pixel color

    // Iterate over each pixel
    for (let i = 0; i < data.length; i += 4) {
        const r_orig = data[i];     // Original red
        const g_orig = data[i + 1]; // Original green
        const b_orig = data[i + 2]; // Original blue
        // Alpha channel data[i+3] is preserved

        // Calculate perceptual luminance (brightness) of the original pixel
        const lum = 0.299 * r_orig + 0.587 * g_orig + 0.114 * b_orig;
        const lumNorm = Math.max(0, Math.min(1, lum / 255.0)); // Normalize luminance to [0, 1]

        // Calculate weights for shadow and highlight colors based on luminance and amount
        const shadowWeight = (1.0 - lumNorm) * numAmount;
        const highlightWeight = lumNorm * numAmount;

        // Blend original pixel with shadow and highlight colors
        let newR = r_orig * originalWeight + sR * shadowWeight + hR * highlightWeight;
        let newG = g_orig * originalWeight + sG * shadowWeight + hG * highlightWeight;
        let newB = b_orig * originalWeight + sB * shadowWeight + hB * highlightWeight;
        
        // Assign new RGB values, ensuring they are clamped to [0, 255]
        data[i]   = Math.max(0, Math.min(255, Math.round(newR)));
        data[i+1] = Math.max(0, Math.min(255, Math.round(newG)));
        data[i+2] = Math.max(0, Math.min(255, Math.round(newB)));
    }

    // Put 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 Split Tone Filter tool allows users to enhance their images by applying a split tone effect. This effect applies a shadow color and a highlight color to the different tonal areas of the image, enabling a unique artistic expression. Users can customize the shadow and highlight colors, as well as control the intensity of the effect. This tool is useful for photographers and artists looking to create visually striking images or achieve a specific mood, making it ideal for enhancing portraits, landscapes, or any creative projects.

Leave a Reply

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