Please bookmark this page to avoid losing your image tool!

Image Captain’s Naval Journal Creator

(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,
    journalDateStr = "AUTO_DATE",
    journalLocation = "At Sea, Unknown Longitude & Latitude",
    entryText = "A most peculiar day. The sea, usually a turbulent mistress, was calm as a millpond. Observed a school of luminous fish unlike any recorded in the charts. The men are uneasy, whispering tales of the deep. I maintain a stoic front, though a sense of wonder, and perhaps a little dread, touches even this old captain's heart. We mark our position and pray for fair winds tomorrow.",
    captainName = "Captain [Your Name]",
    fontNameParam = "IM Fell English SC",
    fontSize = 18,
    textColor = "#3A2A1D", // Dark Sepia
    paperColor = "#F5F5DC", // Beige
    imagePlacement = "top-left", // "none", "top-left", "top-right"
    imageScale = 0.25,
    canvasWidth = 800,
    canvasHeight = 1000
) {

    const fontName = `'${fontNameParam}', serif`; // Add generic fallback

    // Helper function to get current date string
    function getLocalCurrentDate() {
        const d = new Date();
        const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
        return `${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`;
    }

    if (journalDateStr === "AUTO_DATE") {
        journalDateStr = getLocalCurrentDate();
    }

    // Helper function to load font
    async function loadFont(fontFamily, fontUrl) {
        const styleId = `font-style-${fontFamily.replace(/\s+/g, '-')}`;
        if (document.getElementById(styleId)) {
            try {
                await document.fonts.load(`1em "${fontFamily}"`); // Ensure quotes for font names with spaces
                return;
            } catch (e) {
                console.warn(`Re-checking loaded font ${fontFamily} failed, will try to load via CSS. Error:`, e);
            }
        }
        
        if (document.fonts && typeof document.fonts.check === 'function' && document.fonts.check(`1em "${fontFamily}"`)) {
             return; // Font already available
        }

        return new Promise((resolve, reject) => {
            const link = document.createElement('link');
            link.id = styleId;
            link.rel = 'stylesheet';
            link.href = fontUrl;
            link.onload = () => {
                document.fonts.load(`1em "${fontFamily}"`).then(resolve).catch(e => {
                    console.warn(`Failed to load ${fontFamily} via document.fonts.load after CSS link. Error:`, e);
                    // Fallback to a timeout if document.fonts.load is problematic for some reason
                    setTimeout(resolve, 500); // Wait a bit for browser to apply CSS
                });
            };
            link.onerror = (err) => {
                console.error(`Failed to load font CSS from ${fontUrl}`, err);
                reject(err);
            };
            document.head.appendChild(link);
        });
    }

    // Helper function for text wrapping - returns one line and remaining text
    function getNextLine(context, text, maxWidth, currentFont) {
        context.font = currentFont; // Ensure context has the correct font for measurement
        const words = text.split(' ');
        let currentLine = words[0] || "";
        let remainingText = words.slice(1).join(" ");

        if (!words[0]) return { line: "", remainingText: "" };

        // Check if the first word itself is too long
        if (context.measureText(currentLine).width > maxWidth) {
            // Handle very long first word: break it by character (simple approach)
            let fittedChars = "";
            for (let char of currentLine) {
                if (context.measureText(fittedChars + char).width > maxWidth) {
                    break;
                }
                fittedChars += char;
            }
            if (fittedChars === "") { // Cannot even fit one character
                return { line: currentLine[0] || "", remainingText: currentLine.substring(1) + (remainingText ? " " + remainingText : "")  }; // return first char
            }
            remainingText = currentLine.substring(fittedChars.length) + (remainingText ? " " + remainingText : "");
            currentLine = fittedChars;
            return { line: currentLine, remainingText: remainingText.trim() };
        }

        for (let i = 1; i < words.length; i++) {
            const word = words[i];
            const testLine = currentLine + " " + word;
            if (context.measureText(testLine).width <= maxWidth) {
                currentLine = testLine;
                remainingText = words.slice(i + 1).join(" ");
            } else {
                break;
            }
        }
        return { line: currentLine, remainingText: remainingText.trim() };
    }

    // Helper for paper texture (subtle noise)
    function drawPaperTexture(ctx, width, height) {
        const intensity = 25; 
        const tempCanvas = document.createElement('canvas');
        tempCanvas.width = width;
        tempCanvas.height = height;
        const tempCtx = tempCanvas.getContext('2d');
        const imageData = tempCtx.createImageData(width, height);
        const data = imageData.data;

        for (let i = 0; i < data.length; i += 4) {
            const rand = (Math.random() - 0.5) * intensity;
            data[i] = 0; 
            data[i + 1] = 0; 
            data[i + 2] = 0; 
            data[i + 3] = Math.max(0, Math.min(255, 10 + rand)); 
        }
        tempCtx.putImageData(imageData, 0, 0);

        ctx.save();
        ctx.globalCompositeOperation = 'multiply';
        ctx.globalAlpha = 0.15; 
        ctx.drawImage(tempCanvas, 0, 0);
        ctx.restore();
    }

    // 1. Load custom font if specified
    if (fontNameParam === "IM Fell English SC") {
        try {
            await loadFont("IM Fell English SC", "https://fonts.googleapis.com/css2?family=IM+Fell+English+SC&display=swap");
        } catch (e) {
            console.warn("IM Fell English SC font failed to load, fallback to serif will be used.", e);
        }
    }

    // 2. Canvas Setup
    const canvas = document.createElement('canvas');
    canvas.width = canvasWidth;
    canvas.height = canvasHeight;
    const ctx = canvas.getContext('2d');

    // 3. Draw Background Paper
    ctx.fillStyle = paperColor;
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    drawPaperTexture(ctx, canvas.width, canvas.height);


    // 4. Define layout constants
    const padding = Math.max(20, Math.min(canvasWidth, canvasHeight) * 0.05); // Responsive padding
    const lineHeight = fontSize * 1.6; // Increased for script font
    const headerFontSize = fontSize * 0.9;
    const signatureFontSize = fontSize * 1.1;
    let currentY = padding + headerFontSize; // Start Y for header text

    // 5. Draw Date and Location (Header)
    ctx.fillStyle = textColor;
    ctx.font = `${headerFontSize}px ${fontName}`;
    ctx.textAlign = 'right';
    ctx.fillText(journalDateStr, canvas.width - padding, currentY);
    currentY += lineHeight * 0.9;
    ctx.fillText(journalLocation, canvas.width - padding, currentY);
    currentY += lineHeight * 1.5; // Extra space after header

    // 6. Handle Original Image and Main Text Flow
    ctx.textAlign = 'left'; // Reset alignment for main text
    let textCursorY = currentY;
    let remainingTextContent = entryText;

    let imgActualX = 0, imgActualY = 0, imgActualWidth = 0, imgActualHeight = 0;
    let hasSideImage = false;
    const imageRendered = originalImg && originalImg.width > 0 && originalImg.height > 0 && imagePlacement !== "none" && imageScale > 0;

    if (imageRendered) {
        const scaledWidth = originalImg.width * imageScale;
        const scaledHeight = originalImg.height * imageScale;

        if (imagePlacement === "top-left") {
            let potentialTextWidth = canvas.width - (padding + scaledWidth + padding / 2) - padding;
            if (potentialTextWidth >= 100) {
                imgActualX = padding;
                imgActualY = textCursorY;
                imgActualWidth = scaledWidth;
                imgActualHeight = scaledHeight;
                ctx.drawImage(originalImg, imgActualX, imgActualY, imgActualWidth, imgActualHeight);
                hasSideImage = true;
            } else {
                ctx.drawImage(originalImg, (canvas.width - scaledWidth) / 2, textCursorY, scaledWidth, scaledHeight);
                textCursorY += scaledHeight + lineHeight;
            }
        } else if (imagePlacement === "top-right") {
            let potentialTextWidth = (canvas.width - padding - scaledWidth - padding / 2) - padding;
            if (potentialTextWidth >= 100) {
                imgActualX = canvas.width - padding - scaledWidth;
                imgActualY = textCursorY;
                imgActualWidth = scaledWidth;
                imgActualHeight = scaledHeight;
                ctx.drawImage(originalImg, imgActualX, imgActualY, imgActualWidth, imgActualHeight);
                hasSideImage = true;
            } else {
                ctx.drawImage(originalImg, (canvas.width - scaledWidth) / 2, textCursorY, scaledWidth, scaledHeight);
                textCursorY += scaledHeight + lineHeight;
            }
        }
    }

    const imageBottomY = hasSideImage ? (imgActualY + imgActualHeight) : textCursorY;
    const mainTextFont = `${fontSize}px ${fontName}`;
    ctx.font = mainTextFont; // Set font once for main text block initial setup

    while (remainingTextContent.trim() !== "" && textCursorY < canvas.height - padding - lineHeight * 2) {
        let currentLineMaxWidth;
        let currentLineX;

        if (hasSideImage && textCursorY < imageBottomY) {
            if (imagePlacement === "top-left") {
                currentLineX = imgActualX + imgActualWidth + padding / 2;
                currentLineMaxWidth = canvas.width - currentLineX - padding;
            } else { // top-right
                currentLineX = padding;
                currentLineMaxWidth = imgActualX - padding / 2 - padding; // Width available left of image
            }
             if (currentLineMaxWidth < 50) { // Not enough space beside image for meaningful text
                textCursorY = imageBottomY + lineHeight * 0.2; // Move below image
                currentLineX = padding;
                currentLineMaxWidth = canvas.width - 2 * padding;
            }
        } else {
            currentLineX = padding;
            currentLineMaxWidth = canvas.width - 2 * padding;
             if(hasSideImage && textCursorY >= imageBottomY && textCursorY < imageBottomY + lineHeight){
                // Ensure text starts cleanly below the image
                textCursorY = imageBottomY + lineHeight * 0.2;
             }
        }
        
        if (currentLineMaxWidth <=0) { // No space to draw
             console.warn("Calculated text width is zero or negative. Stopping text rendering.");
             break;
        }


        const lineResult = getNextLine(ctx, remainingTextContent, currentLineMaxWidth, mainTextFont);
        
        if (lineResult.line.trim() === "" && remainingTextContent.trim() !== "") {
            console.warn("Cannot fit more text or stuck. Remaining:", remainingTextContent.substring(0,30));
            break; 
        }
        if (lineResult.line.trim() !== "") {
           ctx.fillText(lineResult.line, currentLineX, textCursorY);
        }
        
        remainingTextContent = lineResult.remainingText;
        textCursorY += lineHeight;

        if (textCursorY > canvas.height - padding - (lineHeight * 3)) break; // Safety break near bottom before captain name
    }
    currentY = textCursorY; // Update overall Y cursor

    // 8. Draw Captain's Signature
    ctx.font = `italic ${signatureFontSize}px ${fontName}`;
    ctx.textAlign = 'right';
    // Ensure signature is on page, potentially adjusting currentY if too low
    let signatureY = Math.min(canvas.height - padding, currentY + signatureFontSize);
    if (signatureY < padding + signatureFontSize*2) signatureY = padding + signatureFontSize*2; // Ensure it is not on top

    ctx.fillText(captainName, canvas.width - padding, signatureY);

    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 Captain’s Naval Journal Creator is an online tool that allows users to create beautifully formatted naval journal entries. With this tool, you can upload an image such as a ship or sea scene, and customize your journal entry with details including the date, location, and descriptive text. Ideal for writers, artists, or anyone interested in maritime history, this tool enhances your original images by combining them with stylized text in a vintage-style journal layout. Users can select the font, text color, and paper color to create a unique entry, making it perfect for personal projects, storytelling, or educational purposes.

Leave a Reply

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

Other Image Tools:

Image Renaissance Painting Frame Creator

Image Lost Civilization Artifact Creator

Image Da Vinci Notebook Page Creator

Image Dystopian Citizen ID Creator

Image Monster Hunter Bestiary Creator

Image Vintage Carnival Sideshow Poster Creator

Image Space Explorer’s Log Creator

Image Neolithic Petroglyph Frame Creator

Image Ukiyo-e Japanese Woodblock Print Creator

Image Persian Miniature Painting Creator

Image Sci-Fi Movie Poster Template Creator

Image Horror Movie Poster Template

Image Social Media Milestone Certificate Creator

Halloween Death Certificate Template

Image Anatomical Illustration Frame Creator

Image Romance Novel Cover Template Creator

Image Tabloid Headline Template

Image Space Mission Patch Template Creator

Image Cassette Tape Cover Template Creator

Image Passport Page Template Generator

Image Old Map Frame With Compass Rose Decorator

Image Diploma and Degree Certificate Framer

Image Soviet Propaganda Poster Style Generator

Image Yu-Gi-Oh Card Template Creator

Image Ancient Roman Greek Tablet Frame Creator

Image Marriage Certificate Template Creator

Image Video Game Achievement Frame Creator

Image Newspaper Front Page Template Creator

Image Botanical Illustration Frame Creator

Image Vinyl Record Sleeve Template Creator

Vintage Photo Booth Strip Template Generator

Image Cyberpunk Interface Frame Designer

Image Detective Novel Cover Template

Image Achievement Certificate Framer

Image Illuminated Manuscript Frame Generator

Image Art Deco Poster Frame Creator

See All →