Please bookmark this page to avoid losing your image tool!

Noir Film Poster Design Template

(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 SHADOW CIPHER",
    tagline = "In a city of secrets, truth is the deadliest weapon.",
    starring = "NICK NOIR, VERONICA VALE, EDDIE 'THE FIXER' MALONE",
    directorText = "J. HUSTON JR.",
    posterWidth = 800,
    posterHeight = 1200,
    titleColor = "white",
    taglineColor = "#e0e0e0",
    starringColor = "white",
    directorColor = "#c0c0c0",
    backgroundColor = "#050505",
    contrast = 1.9,
    grainAmount = 25, // 0 for no grain, higher for more visible grain
    vignetteIntensity = 0.65 // 0 for no vignette, 1 for very strong
) {
    const FONT_NAME = 'Bebas Neue';
    let fontLoaded = false;

    // Helper to check if font is loaded via CSS Font Loading API
    const checkFontLoaded = async () => {
        try {
            // Check for at least one character in the font
            return await document.fonts.load(`1em "${FONT_NAME}"`);
        } catch (e) {
            // Error (e.g., API not supported, though unlikely in modern browsers)
            console.warn("Font loading API check failed:", e);
            return false;
        }
    };
    
    // Try to load font if already linked or system font
    if (await checkFontLoaded()) {
        fontLoaded = true;
    }

    // If not loaded, dynamically add the font link
    if (!fontLoaded && !document.querySelector(`link[href*="${FONT_NAME.replace(' ', '+')}"]`)) {
        const link = document.createElement('link');
        link.href = `https://fonts.googleapis.com/css2?family=${FONT_NAME.replace(' ', '+')}:wght@400&display=swap`;
        link.rel = 'stylesheet';
        
        const promiseFontLinkLoad = new Promise((resolve) => {
            link.onload = () => resolve(true);
            link.onerror = () => resolve(false); // Stylesheet failed to load
        });
        document.head.appendChild(link);
        
        if (await promiseFontLinkLoad) {
            // After stylesheet loads, try to confirm with Font Loading API
            if (await checkFontLoaded()) {
                 fontLoaded = true;
            } else {
                // Fallback: Assume font might be usable after a short delay if API check fails post-load
                await new Promise(resolve => setTimeout(resolve, 300)); 
                if (await checkFontLoaded()) fontLoaded = true;
            }
        } else {
             console.warn(`Failed to load stylesheet for font ${FONT_NAME}.`);
        }
    }
    if (!fontLoaded) {
         console.warn(`Font ${FONT_NAME} not available or not confirmed loaded. Using fallbacks.`);
    }

    const canvas = document.createElement('canvas');
    canvas.width = posterWidth;
    canvas.height = posterHeight;
    const ctx = canvas.getContext('2d');

    // Fill background (acts as an underlay if image has transparency or doesn't fully cover)
    ctx.fillStyle = backgroundColor;
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    // Create an offscreen canvas for image manipulation
    const imgCanvas = document.createElement('canvas');
    imgCanvas.width = originalImg.naturalWidth; // Use natural dimensions for full quality processing
    imgCanvas.height = originalImg.naturalHeight;
    const imgCtx = imgCanvas.getContext('2d', { willReadFrequently: true }); // Hint for performance

    // Draw original image to offscreen canvas
    imgCtx.drawImage(originalImg, 0, 0);

    // Apply Grayscale & Contrast
    const imageData = imgCtx.getImageData(0, 0, imgCanvas.width, imgCanvas.height);
    const data = imageData.data;
    for (let i = 0; i < data.length; i += 4) {
        const r = data[i];
        const g = data[i + 1];
        const b = data[i + 2];
        let gray = 0.299 * r + 0.587 * g + 0.114 * b; // Standard luminance calculation
        
        // Apply contrast
        gray = contrast * (gray - 128) + 128;
        gray = Math.max(0, Math.min(255, gray)); // Clamp to 0-255 range

        data[i] = gray;     // Red
        data[i + 1] = gray; // Green
        data[i + 2] = gray; // Blue
    }
    imgCtx.putImageData(imageData, 0, 0);

    // Draw processed image onto main canvas, scaled to cover
    const canvasAspect = canvas.width / canvas.height;
    const processedImgAspect = imgCanvas.width / imgCanvas.height;
    let sx = 0, sy = 0, sWidth = imgCanvas.width, sHeight = imgCanvas.height;

    if (processedImgAspect > canvasAspect) { // Image is wider than canvas target aspect
        sWidth = imgCanvas.height * canvasAspect;
        sx = (imgCanvas.width - sWidth) / 2;
    } else { // Image is taller or same aspect
        sHeight = imgCanvas.width / canvasAspect;
        sy = (imgCanvas.height - sHeight) / 2;
    }
    ctx.drawImage(imgCanvas, sx, sy, sWidth, sHeight, 0, 0, canvas.width, canvas.height);

    // Apply vignette (optional)
    if (vignetteIntensity > 0) {
        const outerRadius = Math.sqrt(canvas.width * canvas.width + canvas.height * canvas.height) / 2; // Diagonal half
        const gradient = ctx.createRadialGradient(
            canvas.width / 2, canvas.height / 2, canvas.height / 3.5, // Inner circle
            canvas.width / 2, canvas.height / 2, outerRadius * 0.85  // Outer circle (adjust factor for spread)
        );
        gradient.addColorStop(0, `rgba(0,0,0,0)`);
        gradient.addColorStop(1, `rgba(0,0,0,${Math.min(1, vignetteIntensity)})`); // Intensity capped at 1
        ctx.fillStyle = gradient;
        ctx.fillRect(0, 0, canvas.width, canvas.height);
    }

    // Apply Film Grain (optional, applied to background before text)
    if (grainAmount > 0) {
        const bgImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
        const bgData = bgImageData.data;
        for (let i = 0; i < bgData.length; i += 4) {
            const noise = (Math.random() - 0.5) * grainAmount;
            bgData[i] = Math.max(0, Math.min(255, bgData[i] + noise));
            bgData[i + 1] = Math.max(0, Math.min(255, bgData[i + 1] + noise));
            bgData[i + 2] = Math.max(0, Math.min(255, bgData[i + 2] + noise));
        }
        ctx.putImageData(bgImageData, 0, 0);
    }
    
    // Text rendering setup
    const FONT_PRIMARY = fontLoaded ? `"${FONT_NAME}"` : "Impact";
    const FONT_FALLBACK = "Arial, 'Helvetica Neue', Helvetica, sans-serif";

    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle'; // Vertically align text to its center

    // TITLE
    const titleSize = Math.floor(canvas.width / 8); // Responsive font size
    ctx.font = `bold ${titleSize}px ${FONT_PRIMARY}, ${FONT_FALLBACK}`; // Add fallback
    ctx.fillStyle = titleColor;
    ctx.shadowColor = 'rgba(0, 0, 0, 0.9)';
    ctx.shadowBlur = 12;
    ctx.shadowOffsetX = 4;
    ctx.shadowOffsetY = 4;

    const titleWords = title.toUpperCase().split(' ');
    let currentLine = '';
    const titleLines = [];
    const maxTitleWidth = canvas.width * 0.9; // Max width for a title line
    for (let n = 0; n < titleWords.length; n++) {
        const testLine = currentLine + titleWords[n] + ' ';
        if (ctx.measureText(testLine.trim()).width > maxTitleWidth && n > 0 && currentLine) {
            titleLines.push(currentLine.trim());
            currentLine = titleWords[n] + ' ';
        } else {
            currentLine = testLine;
        }
    }
    if (currentLine.trim()) titleLines.push(currentLine.trim());

    const lineHeightTitle = titleSize * 0.9;
    const totalTitleHeightEst = titleLines.length * lineHeightTitle;
    let titleBlockStartY = (canvas.height * 0.22) - (totalTitleHeightEst / 2); // Center block around 22% from top
    if (titleLines.length === 1) titleBlockStartY = canvas.height * 0.20;
    else if (titleLines.length > 2) titleBlockStartY = canvas.height * 0.18; // Move up if more lines

    titleLines.forEach((line, index) => {
        // Add half line height because textBaseline is middle
        ctx.fillText(line, canvas.width / 2, titleBlockStartY + (index * lineHeightTitle) + lineHeightTitle / 2);
    });
    const titleBlockEndY = titleBlockStartY + totalTitleHeightEst;

    // TAGLINE
    const taglineSize = Math.floor(canvas.width / 32);
    ctx.font = `italic ${taglineSize}px ${FONT_FALLBACK}`; // Use a more standard font for tagline
    ctx.fillStyle = taglineColor;
    ctx.shadowColor = 'rgba(0,0,0,0.7)';
    ctx.shadowBlur = 6;
    ctx.shadowOffsetX = 2;
    ctx.shadowOffsetY = 2;
    
    const taglineText = tagline.toUpperCase();
    const maxTaglineWidth = canvas.width * 0.7;
    const taglineWords = taglineText.split(' ');
    let taglineLine = "";
    const taglineLines = [];
    for(let i=0; i<taglineWords.length; i++){
        let testLine = taglineLine + taglineWords[i] + " ";
        if(ctx.measureText(testLine.trim()).width > maxTaglineWidth && i > 0 && taglineLine){
            taglineLines.push(taglineLine.trim());
            taglineLine = taglineWords[i] + " ";
        } else {
            taglineLine = testLine;
        }
    }
    if (taglineLine.trim()) taglineLines.push(taglineLine.trim());

    let currentTaglineY = titleBlockEndY + taglineSize * 1.5; // Start below title
    taglineLines.forEach((line, index) => {
         ctx.fillText(line, canvas.width / 2, currentTaglineY + index * taglineSize * 1.1 + taglineSize * 0.5);
    });
    
    ctx.shadowColor = 'transparent'; // Reset shadow for subsequent text elements

    // STARRING
    let currentY = canvas.height * 0.72; 
    const starringHeaderSize = Math.floor(canvas.width / 26);
    ctx.font = `bold ${starringHeaderSize}px ${FONT_PRIMARY}, ${FONT_FALLBACK}`;
    ctx.fillStyle = starringColor;
    ctx.shadowColor = 'rgba(0, 0, 0, 0.6)'; ctx.shadowBlur = 4; ctx.shadowOffsetX = 1; ctx.shadowOffsetY = 1;
    ctx.fillText("STARRING", canvas.width / 2, currentY + starringHeaderSize / 2);
    currentY += starringHeaderSize * 1.3; // Space after "STARRING"

    const starsArray = starring.toUpperCase().split(',').map(s => s.trim()).filter(s => s.length > 0);
    const starringNameSize = Math.floor(canvas.width / 30);
    ctx.font = `${starringNameSize}px ${FONT_PRIMARY}, ${FONT_FALLBACK}`;
    ctx.fillStyle = starringColor; 
    const maxStarringWidth = canvas.width * 0.85;
    
    const starLineTexts = [];
    let currentStarLine = "";
    starsArray.forEach((name) => {
        let prospectiveLine = currentStarLine ? currentStarLine + ", " + name : name;
        if (ctx.measureText(prospectiveLine).width > maxStarringWidth && currentStarLine) {
            starLineTexts.push(currentStarLine);
            currentStarLine = name;
        } else {
            currentStarLine = prospectiveLine;
        }
    });
    if (currentStarLine) starLineTexts.push(currentStarLine);

    starLineTexts.forEach((line, index) => {
        ctx.fillText(line, canvas.width / 2, currentY + index * starringNameSize * 1.1 + starringNameSize / 2);
    });
    
    ctx.shadowColor = 'transparent';

    // DIRECTOR
    const directorY = canvas.height * 0.96; // Near bottom
    const directorSize = Math.floor(canvas.width / 45);
    ctx.font = `${directorSize}px ${FONT_FALLBACK}`;
    ctx.fillStyle = directorColor;
    ctx.fillText("Directed by " + directorText.toUpperCase(), canvas.width / 2, directorY + directorSize / 2);

    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 Noir Film Poster Design Template tool allows users to create stylized noir-themed movie posters by processing an uploaded image. It offers customizable options such as title, tagline, starring cast, and director information, which can be rendered in a selected font and color scheme. Users can also apply various effects like grayscale conversion, contrast adjustment, vignette, and film grain enhancements to achieve an authentic vintage look. This tool is ideal for graphic designers, filmmakers, or enthusiasts looking to design eye-catching promotional material for films, theatrical productions, or events with a noir aesthetic.

Leave a Reply

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