Please bookmark this page to avoid losing your image tool!

Image Role-Playing Game Character Sheet 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,
    characterName = "Adventurer",
    characterClass = "Warrior",
    level = 1,
    strength = 10,
    dexterity = 10,
    constitution = 10,
    intelligence = 10,
    wisdom = 10,
    charisma = 10,
    hitPoints = 10,
    armorClass = 10,
    fontFamily = "Lora", // Default to Lora, a thematic font
    fontSize = 16,      // Base font size for stats etc. in px
    textColor = "#3b3a39", // Dark grey/brown text
    backgroundColor = "#fdf6e3", // A parchment-like color
    borderColor = "#856731",   // A darker brown for border
    sheetWidth = 800,
    sheetHeight = 600,
    sheetTitle = "Character Record Sheet" // Added a parameter for the title
) {
    // Helper function to draw text
    function _drawTextHelper(ctx, text, x, y, font, color, align = 'left', baseline = 'top') {
        ctx.font = font;
        ctx.fillStyle = color;
        ctx.textAlign = align;
        ctx.textBaseline = baseline;
        ctx.fillText(text, x, y);
    }

    // Helper function to draw an image within a box, maintaining aspect ratio (contain & center)
    function _drawImageContainHelper(ctx, img, boxX, boxY, boxWidth, boxHeight, imgBorderColor) {
        // Ensure image has dimensions, otherwise skip drawing it
        if (!img || img.width === 0 || img.height === 0) {
            console.warn("Image has no dimensions or is not loaded. Drawing placeholder for image box.");
            ctx.save();
            ctx.strokeStyle = imgBorderColor || 'gray';
            ctx.lineWidth = 1;
            ctx.strokeRect(boxX, boxY, boxWidth, boxHeight);
            // Draw a cross or placeholder text
             _drawTextHelper(ctx, "[Image]", boxX + boxWidth / 2, boxY + boxHeight / 2 - fontSize/2 , `${fontSize}px Arial`, 'gray', 'center', 'middle');
            ctx.restore();
            return;
        }

        const imgAspectRatio = img.width / img.height;
        const boxAspectRatio = boxWidth / boxHeight;
        let drawWidth, drawHeight, drawX, drawY;

        if (imgAspectRatio > boxAspectRatio) { // Image is wider relative to box shape
            drawWidth = boxWidth;
            drawHeight = boxWidth / imgAspectRatio;
        } else { // Image is taller relative to box shape or same aspect ratio
            drawHeight = boxHeight;
            drawWidth = boxHeight * imgAspectRatio;
        }

        drawX = boxX + (boxWidth - drawWidth) / 2;
        drawY = boxY + (boxHeight - drawHeight) / 2;

        ctx.drawImage(img, drawX, drawY, drawWidth, drawHeight);
    }

    const systemFonts = ["arial", "verdana", "helvetica", "times new roman", "georgia", "courier new", "cursive", "fantasy", "monospace", "sans-serif", "serif"];
    let actualFontFamily = fontFamily;

    if (!systemFonts.includes(fontFamily.toLowerCase())) {
        let fontUrl = '';
        let fontWeight = 'normal'; // Font weight can also be a parameter if needed
        
        // Add specific known fonts here
        if (fontFamily.toLowerCase() === 'lora') {
            fontUrl = 'https://fonts.gstatic.com/s/lora/v32/0QI6MX1D_JOuGQREALqH-2XzxQ.woff2'; // Lora Regular
        } else if (fontFamily.toLowerCase() === 'medievalsharp') {
            fontUrl = 'https://fonts.gstatic.com/s/medievalsharp/v25/EvONINTwokVWj1H32gS2N_WZAPLpOBXxOdM.woff2';
        }
        // Add more recognized custom fonts or allow passing a URL directly

        if (fontUrl) {
            const customFont = new FontFace(fontFamily, `url(${fontUrl})`, { weight: fontWeight });
            try {
                await customFont.load();
                document.fonts.add(customFont);
            } catch (e) {
                console.warn(`Failed to load font '${fontFamily}' from URL. Falling back to Arial. Error: ${e}`);
                actualFontFamily = 'Arial'; // Fallback
            }
        } else {
             // If font is not in systemFonts and not specially handled, browser will attempt to use it as is.
             // Useful if the font is already loaded by other means or is a generic family name
             console.log(`Using specified font '${fontFamily}' which is not in system fonts list or pre-configured for CDN loading. Browser will attempt to use it.`);
        }
    }


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

    // --- Drawing starts ---

    // Background
    ctx.fillStyle = backgroundColor;
    ctx.fillRect(0, 0, sheetWidth, sheetHeight);

    // Border for the entire sheet
    const mainBorderWidth = 3;
    ctx.strokeStyle = borderColor;
    ctx.lineWidth = mainBorderWidth;
    ctx.strokeRect(mainBorderWidth / 2, mainBorderWidth / 2, sheetWidth - mainBorderWidth, sheetHeight - mainBorderWidth);

    const pagePadding = 20;
    let currentY = pagePadding;

    // 0. Sheet Title
    if (sheetTitle) {
        const titleFontSize = Math.floor(fontSize * 1.8);
        const titleFont = `${titleFontSize}px ${actualFontFamily}`;
        ctx.font = titleFont; // Set font for measurement
        const titleMetrics = ctx.measureText(sheetTitle);
        const titleWidth = titleMetrics.width;
        
        _drawTextHelper(ctx, sheetTitle, (sheetWidth - titleWidth) / 2, currentY, titleFont, textColor, 'left', 'top');
        currentY += titleFontSize + pagePadding;
    }


    // 1. Row 1: Portrait and Main Info
    const imgBoxX = pagePadding;
    const imgBoxY = currentY;
    // Define image box size relative to available space but capped
    const imgBoxWidth = Math.max(100, Math.min(sheetWidth * 0.35, 250)); 
    const imgBoxHeight = Math.max(120, Math.min(sheetHeight * 0.4, 300));

    _drawImageContainHelper(ctx, originalImg, imgBoxX, imgBoxY, imgBoxWidth, imgBoxHeight, borderColor);
    ctx.strokeStyle = borderColor;
    ctx.lineWidth = 1; // Thinner border for internal elements like the image box
    ctx.strokeRect(imgBoxX, imgBoxY, imgBoxWidth, imgBoxHeight);

    // Main Info (Name, Class, Level, HP, AC) - to the right of portrait
    const infoX = imgBoxX + imgBoxWidth + pagePadding;
    let infoCurrentY = currentY; // Y tracker for this column
    // const infoWidth = sheetWidth - infoX - pagePadding; // Available width for this column

    // Character Name
    const nameFontSize = Math.floor(fontSize * 1.7); // Slightly adjusted
    _drawTextHelper(ctx, characterName, infoX, infoCurrentY, `${nameFontSize}px ${actualFontFamily}`, textColor, 'left', 'top');
    infoCurrentY += nameFontSize + 8; // Gap

    // Class & Level
    const classLevelFontSize = Math.floor(fontSize * 1.15); // Slightly adjusted
    _drawTextHelper(ctx, `${characterClass} - Level ${level}`, infoX, infoCurrentY, `${classLevelFontSize}px ${actualFontFamily}`, textColor, 'left', 'top');
    infoCurrentY += classLevelFontSize + 15; // Larger gap before combat stats

    // HP
    const statTextFontSize = fontSize; // Base font size for these stats
    _drawTextHelper(ctx, `Hit Points: ${hitPoints}`, infoX, infoCurrentY, `${statTextFontSize}px ${actualFontFamily}`, textColor, 'left', 'top');
    infoCurrentY += statTextFontSize + 8;

    // AC
    _drawTextHelper(ctx, `Armor Class: ${armorClass}`, infoX, infoCurrentY, `${statTextFontSize}px ${actualFontFamily}`, textColor, 'left', 'top');
    infoCurrentY += statTextFontSize + 8;

    // Update main currentY to be below the taller of portrait or info block
    currentY = Math.max(imgBoxY + imgBoxHeight, infoCurrentY) + pagePadding;


    // 2. Row 2: Attributes/Stats
    const statsSectionY = currentY;
    const statsLabelStyle = `${fontSize}px ${actualFontFamily}`;
    const statsValueStyle = `${fontSize}px ${actualFontFamily}`;

    ctx.font = statsLabelStyle; // Set font for measurements
    const statFullLabels = ["Strength", "Dexterity", "Constitution", "Intelligence", "Wisdom", "Charisma"];
    const statValues = [strength, dexterity, constitution, intelligence, wisdom, charisma];

    let maxStatLabelWidth = 0;
    statFullLabels.forEach(label => {
        maxStatLabelWidth = Math.max(maxStatLabelWidth, ctx.measureText(label + ":").width);
    });
    maxStatLabelWidth += 5; // Small padding after label text

    const statsLineHeight = Math.floor(fontSize * 1.6);
    const numStats = statFullLabels.length;
    const statsPerColumn = Math.ceil(numStats / 2);
    const columnGap = 25; // Gap between the two stat columns
    
    // Calculate column width based on available space
    const availableWidthForStats = sheetWidth - 2 * pagePadding - columnGap;
    let statColumnWidth = availableWidthForStats / 2;
    
    if (infoCurrentY > currentY && (imgBoxX + imgBoxWidth + pagePadding > sheetWidth / 2)) {
        // If info column was very tall and portrait is on left, stats might need to start further down.
        // This simple layout currently puts stats always below portrait and info block.
    }


    for (let i = 0; i < numStats; i++) {
        const col = Math.floor(i / statsPerColumn);
        const rowInCol = i % statsPerColumn;

        const xPosLabel = pagePadding + col * (statColumnWidth + columnGap);
        const xPosValue = xPosLabel + maxStatLabelWidth + 10; // 10px space between label and value
        const yPos = statsSectionY + rowInCol * statsLineHeight;

        if (yPos + fontSize > sheetHeight - pagePadding) { 
             console.warn("Content may be overflowing sheet height in stats section.");
             break; 
        }

        _drawTextHelper(ctx, `${statFullLabels[i]}:`, xPosLabel, yPos, statsLabelStyle, textColor, 'left', 'top');
        _drawTextHelper(ctx, statValues[i].toString(), xPosValue, yPos, statsValueStyle, textColor, 'left', 'top');
    }
    
    // Optional: Draw a line or flourish - Example: a simple line
    let finalContentY = statsSectionY + statsPerColumn * statsLineHeight;
    if (finalContentY < sheetHeight - pagePadding - 10) { // Check if there's space
        ctx.beginPath();
        ctx.moveTo(pagePadding, finalContentY + 5);
        ctx.lineTo(sheetWidth - pagePadding, finalContentY + 5);
        ctx.strokeStyle = borderColor;
        ctx.globalAlpha = 0.5; // Make line lighter
        ctx.lineWidth = 0.5;
        ctx.stroke();
        ctx.globalAlpha = 1.0; // Reset alpha
    }

    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 Role-Playing Game Character Sheet Creator tool allows users to generate a custom character sheet for role-playing games. Users can upload an image to serve as a character portrait and personalize various character attributes such as name, class, level, and stats including strength, dexterity, and hit points. The created sheet features a parchment-like background and can include custom styling elements like fonts and colors. This tool is ideal for gamers looking to create visually engaging and organized character sheets for tabletop RPG sessions, enhancing their gaming experience with a professional-looking document.

Leave a Reply

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