Please bookmark this page to avoid losing your image tool!

Image Cinematic 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.
async function processImage(
    originalImg,
    aspectRatio = 0,      // Target aspect ratio (e.g., 2.35 for 2.35:1). 0 or less for original.
    saturation = 0.8,     // Saturation level (0=grayscale, 1=original, >1=oversaturated). Default: 0.8 (slight desaturation).
    contrast = 15,        // Contrast adjustment (-100 to 100). Default: 15 (slight increase).
    tintColor = '#1a2a3a',// CSS color string for tint (e.g., '#FFD700' for warm, '#2a3a4a' for cool). Empty or null for no tint.
    tintOpacity = 0.15,   // Opacity of the tint (0 to 1). Default: 0.15.
    vignetteStrength = 0.5,// Strength of the vignette (0 to 1). Default: 0.5.
    vignetteSoftness = 0.5,// Softness of the vignette (0=hard edge, 1=very soft fading from center). Default: 0.5.
    grainStrength = 0.03  // Strength of film grain (0 to 1). Default: 0.03 (subtle).
) {

    // Ensure valid image dimensions
    if (!originalImg || originalImg.width === 0 || originalImg.height === 0) {
        console.error("Invalid image provided to processImage.");
        const emptyCanvas = document.createElement('canvas');
        emptyCanvas.width = 1; emptyCanvas.height = 1; // Minimal canvas
        return emptyCanvas;
    }

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

    const oAR = originalImg.width / originalImg.height; // Original aspect ratio

    let finalCanvasWidth, finalCanvasHeight;

    // Determine final canvas dimensions based on target aspect ratio
    if (aspectRatio > 0 && Math.abs(oAR - aspectRatio) > 0.001) { // Apply new aspect ratio
        finalCanvasWidth = originalImg.width; // Anchor width
        finalCanvasHeight = Math.round(finalCanvasWidth / aspectRatio);

        // Sanity check: if maintaining originalImg.width makes the image content need upscaling in height to fit
        // (e.g. original is very wide panorama, target AR is tall), it's better to anchor by originalImg.height.
        // This scenario is less common for typical photos/ cinematic aspect ratio changes.
        // The current logic prioritizes using the original image's width.
    } else { // Use original aspect ratio
        finalCanvasWidth = originalImg.width;
        finalCanvasHeight = originalImg.height;
    }

    finalCanvas.width = finalCanvasWidth;
    finalCanvas.height = finalCanvasHeight;

    // Calculate drawing parameters to fit originalImg into finalCanvas, maintaining aspect ratio
    const canvasAR = finalCanvas.width / finalCanvas.height;
    let drawX = 0, drawY = 0, drawWidth = finalCanvas.width, drawHeight = finalCanvas.height;

    if (oAR > canvasAR) { // Original is wider than final canvas (pillarbox needed)
        drawHeight = finalCanvas.height;
        drawWidth = drawHeight * oAR;
        drawX = (finalCanvas.width - drawWidth) / 2;
    } else if (oAR < canvasAR) { // Original is taller than final canvas (letterbox needed)
        drawWidth = finalCanvas.width;
        drawHeight = drawWidth / oAR;
        drawY = (finalCanvas.height - drawHeight) / 2;
    }
    // If oAR == canvasAR, image perfectly fits; drawX, drawY remain 0.

    // Create a temporary canvas to hold the scaled image content for processing
    const tempImageCanvas = document.createElement('canvas');
    tempImageCanvas.width = Math.abs(drawWidth); // Use Math.abs in case of rounding issues causing tiny negatives
    tempImageCanvas.height = Math.abs(drawHeight);
    const tempImageCtx = tempImageCanvas.getContext('2d');
    tempImageCtx.drawImage(originalImg, 0, 0, originalImg.width, originalImg.height, 0, 0, tempImageCanvas.width, tempImageCanvas.height);

    // --- Apply pixel-level adjustments (Saturation, Contrast) ---
    if (saturation !== 1.0 || contrast !== 0) {
        let imgData = tempImageCtx.getImageData(0, 0, tempImageCanvas.width, tempImageCanvas.height);
        let pixels = imgData.data;
        const len = pixels.length;
        const contrastFactor = (259 * (contrast + 255)) / (255 * (259 - contrast));

        for (let i = 0; i < len; i += 4) {
            if (pixels[i + 3] === 0) continue; // Skip fully transparent pixels

            let r = pixels[i];
            let g = pixels[i + 1];
            let b = pixels[i + 2];

            // Saturation
            if (saturation !== 1.0) {
                const gray = 0.299 * r + 0.587 * g + 0.114 * b;
                r = gray + saturation * (r - gray);
                g = gray + saturation * (g - gray);
                b = gray + saturation * (b - gray);
            }

            // Contrast
            if (contrast !== 0) {
                r = contrastFactor * (r - 128) + 128;
                g = contrastFactor * (g - 128) + 128;
                b = contrastFactor * (b - 128) + 128;
            }

            pixels[i] = Math.max(0, Math.min(255, r));
            pixels[i + 1] = Math.max(0, Math.min(255, g));
            pixels[i + 2] = Math.max(0, Math.min(255, b));
        }
        tempImageCtx.putImageData(imgData, 0, 0);
    }

    // Draw the (black) background for letterbox/pillarbox bars on final canvas
    finalCtx.fillStyle = 'black';
    finalCtx.fillRect(0, 0, finalCanvas.width, finalCanvas.height);

    // Draw the processed image content onto the final canvas
    finalCtx.drawImage(tempImageCanvas, drawX, drawY, drawWidth, drawHeight);

    // Determine the actual rectangle where the image content is visible on the final canvas
    const imgRectOnCanvas = {
        x: Math.max(0, drawX),
        y: Math.max(0, drawY),
        width: finalCanvas.width - 2 * Math.max(0, drawX), // If pillarboxed, drawX is negative
        height: finalCanvas.height - 2 * Math.max(0, drawY) // If letterboxed, drawY is negative
    };
     // More robust calculation for imgRectOnCanvas, considering the image content within the final canvas bounds
    if (drawX < 0) { // Pillarboxed (image content is wider than canvas at drawY)
        imgRectOnCanvas.x = 0;
        imgRectOnCanvas.width = finalCanvas.width;
    }
    if (drawY < 0) { // Letterboxed (image content is taller than canvas at drawX)
        imgRectOnCanvas.y = 0;
        imgRectOnCanvas.height = finalCanvas.height;
    }


    // --- Apply Tint ---
    if (tintColor && typeof tintColor === 'string' && tintColor.trim() !== "" && tintOpacity > 0) {
        finalCtx.globalAlpha = tintOpacity;
        finalCtx.fillStyle = tintColor;
        finalCtx.globalCompositeOperation = 'overlay'; // 'color' or 'overlay' or 'multiply' give different feels
        finalCtx.fillRect(imgRectOnCanvas.x, imgRectOnCanvas.y, imgRectOnCanvas.width, imgRectOnCanvas.height);
        finalCtx.globalAlpha = 1.0;
        finalCtx.globalCompositeOperation = 'source-over'; // Reset
    }

    // --- Apply Film Grain ---
    if (grainStrength > 0 && grainStrength <= 1) {
        let contentForGrainData = finalCtx.getImageData(imgRectOnCanvas.x, imgRectOnCanvas.y, imgRectOnCanvas.width, imgRectOnCanvas.height);
        let grainPixels = contentForGrainData.data;
        const grainPixelsLen = grainPixels.length;
        const grainIntensity = 50; // Max pixel value change for grain, scaled by grainStrength

        for (let i = 0; i < grainPixelsLen; i += 4) {
            if (grainPixels[i + 3] === 0) continue; // Skip transparent pixels

            const noise = (Math.random() - 0.5) * 2 * grainIntensity * grainStrength;
            grainPixels[i] = Math.max(0, Math.min(255, grainPixels[i] + noise));
            grainPixels[i + 1] = Math.max(0, Math.min(255, grainPixels[i + 1] + noise));
            grainPixels[i + 2] = Math.max(0, Math.min(255, grainPixels[i + 2] + noise));
        }
        finalCtx.putImageData(contentForGrainData, imgRectOnCanvas.x, imgRectOnCanvas.y);
    }

    // --- Apply Vignette (applied last, over everything else) ---
    if (vignetteStrength > 0) {
        const centerX = finalCanvas.width / 2;
        const centerY = finalCanvas.height / 2;
        const outerRadius = Math.sqrt(centerX * centerX + centerY * centerY);
        // vignetteSoftness: 0=hard edge at outerRadius, 1=gradient starts from center
        const innerRadius = outerRadius * Math.max(0, Math.min(1, 1 - vignetteSoftness)); 

        const grad = finalCtx.createRadialGradient(centerX, centerY, innerRadius, centerX, centerY, outerRadius);
        grad.addColorStop(0, 'rgba(0,0,0,0)'); // Transparent center
        grad.addColorStop(1, `rgba(0,0,0,${vignetteStrength})`); // Dark edges

        finalCtx.fillStyle = grad;
        finalCtx.fillRect(0, 0, finalCanvas.width, finalCanvas.height);
    }

    return finalCanvas;
}

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 Cinematic Filter Application is a versatile tool designed to enhance your images with a range of cinematic effects. Users can adjust aspects such as saturation, contrast, and apply a variety of filters including tinting, vignette effects, and film grain. Ideal for photographers, filmmakers, and graphic designers, this application allows for the transformation of ordinary photos into visually stunning images that evoke a cinematic feel. Whether for enhancing social media postings, creating art, or producing media content, this tool provides a user-friendly way to elevate visual quality.

Leave a Reply

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