Please bookmark this page to avoid losing your image tool!

Image Medieval Bestiary Page 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,
    titleText = "Creatura Mirabilis",
    descriptionText = "Hic habitat bestia mirabilis, formis et coloribus variis insignita. Observate diligenter mores eius et naturam singularem. Longius extenditur narratio ut probetur involutio textus et quomodo se gerat in angustiis spatii.",
    fontName = "IM Fell DW Pica",
    titleFontSize = 36,
    descriptionFontSize = 16,
    textColor = "#3A2A1A", // Dark Brown
    backgroundColor = "#FDF5E6", // Old Lace (parchment-like)
    borderColor = "#8B4513", // SaddleBrown
    pageOuterBorderWidth = 10, 
    imageInnerBorderWidth = 4,
    pagePadding = 50,
    canvasWidth = 800,
    canvasHeight = 1100,
    imageMaxHeight = 350
) {

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

    // Helper function for text wrapping.
    // x: left boundary of the text block.
    // yBaseline: baseline of the first line of text.
    // maxWidth: maximum width available for text lines.
    // Returns: y-coordinate for the baseline of a potential line immediately following the wrapped text.
    function wrapText(context, text, x, yBaseline, maxWidth, lineHeight, textAlign = 'left', initialIndent = 0, indentLinesCount = 0) {
        const words = text.split(' ');
        let line = '';
        let currentLineY = yBaseline;
        let linesDrawn = 0;

        for (let n = 0; n < words.length; n++) {
            const isIndentedLine = linesDrawn < indentLinesCount;
            const currentLineMaxWidth = isIndentedLine ? maxWidth - initialIndent : maxWidth;
            
            const testLine = line + words[n] + ' ';
            const metrics = context.measureText(testLine); // Requires context.font to be set
            const testWidth = metrics.width;

            if (testWidth > currentLineMaxWidth && line !== "") {
                let drawX;
                const trimmedLine = line.trim();
                const currentLineTextWidth = context.measureText(trimmedLine).width;

                if (textAlign === 'center') {
                    drawX = x + (maxWidth - currentLineTextWidth) / 2;
                    if (isIndentedLine) { // Adjust centering for indented lines if necessary (rarely used together)
                         drawX = (x + initialIndent) + ((maxWidth - initialIndent) - currentLineTextWidth) / 2;
                    }
                } else { // 'left'
                    drawX = isIndentedLine ? x + initialIndent : x;
                }
                context.fillText(trimmedLine, drawX, currentLineY);
                line = words[n] + ' ';
                currentLineY += lineHeight;
                linesDrawn++;
            } else {
                line = testLine;
            }
        }
        
        // Draw the last line
        const isLastLineIndented = linesDrawn < indentLinesCount;
        let lastLineDrawX;
        const trimmedLastLine = line.trim();
        const lastLineTextWidth = context.measureText(trimmedLastLine).width;

        if (textAlign === 'center') {
            lastLineDrawX = x + (maxWidth - lastLineTextWidth) / 2;
             if (isLastLineIndented) {
                 lastLineDrawX = (x + initialIndent) + ((maxWidth - initialIndent) - lastLineTextWidth) / 2;
            }
        } else { // 'left'
            lastLineDrawX = isLastLineIndented ? x + initialIndent : x;
        }
        if (trimmedLastLine) { // Avoid drawing empty lines if text ends with spaces
           context.fillText(trimmedLastLine, lastLineDrawX, currentLineY);
        }
        return currentLineY + lineHeight; 
    }

    // Dynamically load the specified font if it's 'IM Fell DW Pica'
    if (fontName === 'IM Fell DW Pica') {
        const styleId = 'im-fell-dw-pica-font-style-dynamic'; // Unique ID for the style tag
        if (!document.getElementById(styleId)) { // Add style tag only once
            const style = document.createElement('style');
            style.id = styleId;
            style.innerHTML = `
                @import url('https://fonts.googleapis.com/css2?family=IM+Fell+DW+Pica:ital@0;1&display=swap');
            `;
            document.head.appendChild(style);
        }
        try {
            // Wait for font to be available. Check a small size.
            await document.fonts.load(`10px "${fontName}"`); 
            await document.fonts.load(`italic 10px "${fontName}"`); // For title and drop cap
        } catch (e) {
            console.warn(`Font "${fontName}" could not be loaded or timed out. Browser will use fallback.`, e);
        }
    }
    
    // 1. Page Background
    ctx.fillStyle = backgroundColor;
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    // Define content area (inside pagePadding)
    const contentAreaX = pagePadding;
    const contentAreaY = pagePadding;
    const contentAreaWidth = canvas.width - 2 * pagePadding;
    const contentAreaHeight = canvas.height - 2 * pagePadding;

    // 2. Main Page Border (drawn around the contentArea)
    ctx.strokeStyle = borderColor;
    ctx.lineWidth = pageOuterBorderWidth;
    ctx.strokeRect(
        contentAreaX - pageOuterBorderWidth / 2, 
        contentAreaY - pageOuterBorderWidth / 2, 
        contentAreaWidth + pageOuterBorderWidth, 
        contentAreaHeight + pageOuterBorderWidth
    );
    
    // Vertical cursor for layout, tracks the top of the current element block
    let currentElementTopY = contentAreaY; 

    // 3. Title
    ctx.font = `italic ${titleFontSize}px "${fontName}", serif`; // Serif fallback
    ctx.fillStyle = textColor;
    const titleLineHeight = titleFontSize * 1.2;
    const titleFirstLineBaselineY = currentElementTopY + titleFontSize; // Baseline for the first line of the title

    // wrapText returns basline for next block, adjust to get bottom of current text block.
    const titleBottomY = wrapText(ctx, titleText, 
        contentAreaX,              
        titleFirstLineBaselineY,   
        contentAreaWidth,          
        titleLineHeight, 
        'center'                   
    ) - titleLineHeight + (titleLineHeight - titleFontSize); // Estimate bottom based on actual text height
    currentElementTopY = titleBottomY + titleFontSize * 0.5; // Add spacing after title

    // 4. Image
    let imgDrawWidth = originalImg.width;
    let imgDrawHeight = originalImg.height;
    const aspectRatio = imgDrawWidth / imgDrawHeight;
    
    // Max width for the image content itself (respecting its own border)
    const imageAllowedWidth = contentAreaWidth - imageInnerBorderWidth * 2; 

    if (imgDrawHeight > imageMaxHeight) { // Scale by global max height for image
        imgDrawHeight = imageMaxHeight;
        imgDrawWidth = imgDrawHeight * aspectRatio;
    }
    if (imgDrawWidth > imageAllowedWidth) { // Scale if too wide for its allowed area
        imgDrawWidth = imageAllowedWidth;
        imgDrawHeight = imgDrawWidth / aspectRatio;
    }

    // Center image horizontally within contentArea
    const imgContentX = contentAreaX + (contentAreaWidth - imgDrawWidth) / 2;
    const imgContentY = currentElementTopY;

    // Draw image border
    ctx.strokeStyle = borderColor;
    ctx.lineWidth = imageInnerBorderWidth;
    ctx.strokeRect(
        imgContentX - imageInnerBorderWidth / 2, 
        imgContentY - imageInnerBorderWidth / 2, 
        imgDrawWidth + imageInnerBorderWidth, 
        imgDrawHeight + imageInnerBorderWidth
    );
    // Draw image
    ctx.drawImage(originalImg, imgContentX, imgContentY, imgDrawWidth, imgDrawHeight);
    
    currentElementTopY = imgContentY + imgDrawHeight + imageInnerBorderWidth; // Update Y to be below image + its border
    currentElementTopY += descriptionFontSize * 1.5; // Add spacing before description

    // 5. Description Text with Drop Cap
    if (descriptionText && descriptionText.length > 0) {
        const descBaseX = contentAreaX; 
        const descMaxWidth = contentAreaWidth;
        const descLineHeight = descriptionFontSize * 1.4;
        
        // Baseline for the first line of normal description text (if no drop cap)
        // or for the first line of text *next to* the drop cap.
        const descFirstLineTextBaselineY = currentElementTopY + descriptionFontSize; 

        const firstLetter = descriptionText[0];
        const restOfText = descriptionText.substring(1);

        // Draw Drop Cap
        const dropCapSize = descriptionFontSize * 3;
        ctx.font = `italic ${dropCapSize}px "${fontName}", serif`;
        ctx.fillStyle = textColor;
        // Position drop cap baseline: currentElementTopY is top of text line, add ~75% of cap size for baseline
        const dropCapBaselineY = currentElementTopY + dropCapSize * 0.75;
        ctx.fillText(firstLetter, descBaseX, dropCapBaselineY); 

        const dropCapMetrics = ctx.measureText(firstLetter); // Uses current (dropCap) font
        const dropCapEffectiveWidth = dropCapMetrics.width + descriptionFontSize * 0.3; // Width of char + small right-side gap
        // Effective height of drop cap from its top alignment point (currentElementTopY)
        const dropCapEffectiveVisualHeight = dropCapSize * 0.8; 

        // How many lines of normal text will be next to the drop cap
        const numIndentLines = Math.ceil(dropCapEffectiveVisualHeight / descLineHeight);

        // Draw rest of the text
        ctx.font = `${descriptionFontSize}px "${fontName}", serif`; // Normal font for description
        
        wrapText(ctx, restOfText,
            descBaseX,                      
            descFirstLineTextBaselineY,     
            descMaxWidth,                   
            descLineHeight,                 
            'left',                         
            dropCapEffectiveWidth,          
            numIndentLines                  
        );
    }

    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 Medieval Bestiary Page Template tool allows users to create visually appealing pages featuring images and text in a medieval bestiary style. Users can input an image and customize the layout with a title, description, text styles, and page design elements such as borders and colors. This tool is ideal for artists, educators, or enthusiasts looking to produce decorative presentations of fantastical creatures and lore, suitable for projects, storytelling, or artistic displays.

Leave a Reply

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