You can edit the below JavaScript code to customize the image tool.
Apply Changes
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;
}
Apply Changes