Please bookmark this page to avoid losing your image tool!

The Conjuring 2 Movie Poster

(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, title = "THE CONJURING 2", topText = "THE NEXT TRUE STORY FROM THE CASE FILES OF ED AND LORRAINE WARREN", bottomText = "BASED ON TRUE EVENTS") {
    // Initialize Canvas
    const canvas = document.createElement('canvas');
    const width = originalImg.width;
    const height = originalImg.height;
    canvas.width = width;
    canvas.height = height;
    const ctx = canvas.getContext('2d');

    // Dynamically Inject and Load "Cinzel" Google Font for Cinematic Typography
    const fontName = 'Cinzel';
    const fontTagId = 'cinzel-font-tag';
    if (!document.getElementById(fontTagId)) {
        const link = document.createElement('link');
        link.id = fontTagId;
        link.href = 'https://fonts.googleapis.com/css2?family=Cinzel:wght@500;700&display=swap';
        link.rel = 'stylesheet';
        document.head.appendChild(link);
    }
    
    try {
        await document.fonts.load(`700 24px "${fontName}"`);
        await document.fonts.load(`500 24px "${fontName}"`);
    } catch (e) {
        // Fallback to standard serif gracefully if it gets blocked
        console.warn("Could not load Google Font. Using fallback 'serif'.");
    }

    // Fill black and Draw Original Image (safeguard for transparent images like PNGs)
    ctx.fillStyle = '#000000';
    ctx.fillRect(0, 0, width, height);
    ctx.drawImage(originalImg, 0, 0);

    // Precompute a pseudo-random Noise Table (optimizing the hot loop by avoiding Math.random())
    const noiseTable = new Float32Array(4096);
    for (let i = 0; i < 4096; i++) {
        noiseTable[i] = (Math.random() - 0.5) * 15;
    }

    // Color Grading: High Contrast & Cyan/Teal Shadows (Cinematic Horror Map)
    const imgData = ctx.getImageData(0, 0, width, height);
    const data = imgData.data;

    const cx = width / 2;
    const cy = height / 2;
    const maxDist = Math.sqrt((width / 2) * (width / 2) + (height / 2) * (height / 2));
    const contrast = 1.25;
    const gradeAmount = 0.75;

    for (let i = 0, len = data.length; i < len; i += 4) {
        let r = data[i];
        let g = data[i + 1];
        let b = data[i + 2];

        // 1. Boosted Contrast
        r = ((r / 255 - 0.5) * contrast + 0.5) * 255;
        g = ((g / 255 - 0.5) * contrast + 0.5) * 255;
        b = ((b / 255 - 0.5) * contrast + 0.5) * 255;

        // Calculate Luminance
        const lum = 0.299 * r + 0.587 * g + 0.114 * b;

        // 2. Map to eerie cyan/blue 3-stop gradient
        let tr, tg, tb;
        if (lum < 85) { // Shadows
            const t = lum / 85;
            tr = 2 + 25 * t;
            tg = 8 + 45 * t;
            tb = 15 + 60 * t;
        } else if (lum < 170) { // Midtones
            const t = (lum - 85) / 85;
            tr = 27 + 60 * t;
            tg = 53 + 70 * t;
            tb = 75 + 75 * t;
        } else { // Highlights
            const t = (lum - 170) / 85;
            tr = 87 + 158 * t;
            tg = 123 + 127 * t;
            tb = 150 + 105 * t;
        }

        // 3. Blend cinematic grade back into contrast-boosted original
        r = r * (1 - gradeAmount) + tr * gradeAmount;
        g = g * (1 - gradeAmount) + tg * gradeAmount;
        b = b * (1 - gradeAmount) + tb * gradeAmount;

        // 4. Eerie Vignette (Slightly raised center for character portraits)
        const pxl = i >> 2;
        const x = pxl % width;
        const y = Math.floor(pxl / width);
        const dx = x - cx;
        const dy = y - (cy * 0.9); 
        
        const dist = Math.sqrt(dx * dx + dy * dy); // Usually faster than Math.hypot in JS
        
        let vignette = 1 - (dist / maxDist);
        if (vignette < 0) vignette = 0;
        
        vignette = Math.pow(vignette, 0.7); 
        vignette = 0.15 + 0.85 * vignette; // Soften extreme edge shadows slightly

        // 5. Apply Film Grain
        const noise = noiseTable[(x * 17 + y * 53) & 4095] * vignette;

        // Assign back (values auto-clamp within Uint8ClampedArray bounds internally)
        data[i] = (r * vignette) + noise;
        data[i + 1] = (g * vignette) + noise;
        data[i + 2] = (b * vignette) + noise;
    }
    
    ctx.putImageData(imgData, 0, 0);

    // ----------------------------------------------------- //
    // Typography System (Spacing and Fitting Engine) 
    // ----------------------------------------------------- //
    
    function getSpacedWidth(context, txt, size, weight, spacingRatio) {
        if (!txt) return 0;
        context.font = `${weight} ${size}px "${fontName}", serif`;
        let w = 0;
        for (let char of txt) w += context.measureText(char).width;
        return w + (size * spacingRatio * (txt.length - 1));
    }

    function fillTextSpaced(context, txt, x, y, letterSpacing) {
        if (!txt) return;
        const chars = txt.split('');
        let totalWidth = 0;
        const widths = new Float32Array(chars.length);
        for (let i = 0; i < chars.length; i++) {
            const w = context.measureText(chars[i]).width;
            widths[i] = w;
            totalWidth += w;
        }
        totalWidth += letterSpacing * (chars.length - 1);
        
        let currentX = x - totalWidth / 2;
        for (let i = 0; i < chars.length; i++) {
            context.fillText(chars[i], currentX, y);
            currentX += widths[i] + letterSpacing;
        }
    }

    ctx.shadowColor = 'rgba(0, 0, 0, 0.9)';
    ctx.shadowBlur = Math.max(width, height) * 0.01;
    ctx.shadowOffsetX = 0;
    ctx.shadowOffsetY = Math.max(width, height) * 0.003;
    ctx.textAlign = 'left'; 
    ctx.textBaseline = 'middle';

    // Calculate dynamic scaling constraints for multi-tier text 
    let titleSize = width * 0.09;
    let titleWidth = getSpacedWidth(ctx, title, titleSize, '700', 0.15);
    if (titleWidth > width * 0.9 && titleWidth > 0) {
        titleSize *= (width * 0.9) / titleWidth; // proportional scaling protects from freezing while-loops
    }

    let topTextSize = width * 0.0225;
    let topWidth = getSpacedWidth(ctx, topText, topTextSize, '500', 0.2);
    if (topWidth > width * 0.9 && topWidth > 0) {
        topTextSize *= (width * 0.9) / topWidth;
    }

    let bottomTextSize = width * 0.0315;
    let botWidth = getSpacedWidth(ctx, bottomText, bottomTextSize, '500', 0.25);
    if (botWidth > width * 0.85 && botWidth > 0) {
        bottomTextSize *= (width * 0.85) / botWidth;
    }

    // Render layers
    if (topText) {
        ctx.font = `500 ${topTextSize}px "${fontName}", serif`;
        ctx.fillStyle = 'rgba(210, 220, 230, 0.85)'; // Bleak blue/gray
        fillTextSpaced(ctx, topText.toUpperCase(), width / 2, height * 0.72, topTextSize * 0.2);
    }

    if (title) {
        ctx.font = `700 ${titleSize}px "${fontName}", serif`;
        ctx.fillStyle = 'rgba(245, 250, 255, 0.95)'; // Pale chill white
        fillTextSpaced(ctx, title.toUpperCase(), width / 2, height * 0.8, titleSize * 0.15);
    }

    if (bottomText) {
        ctx.font = `500 ${bottomTextSize}px "${fontName}", serif`;
        ctx.fillStyle = 'rgba(180, 195, 210, 0.9)';
        fillTextSpaced(ctx, bottomText.toUpperCase(), width / 2, height * 0.88, bottomTextSize * 0.25);
    }

    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

This tool transforms your images into cinematic horror-style movie posters. It applies advanced color grading to create an eerie atmosphere using high contrast and cyan/teal shadows, adds a dark vignette, and introduces film grain for a vintage texture. Additionally, it overlays stylized, spaced typography including a custom title and tagline, making it ideal for creators looking to give their photos a dramatic, suspenseful film aesthetic.

Leave a Reply

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