You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
monsterName = "Unknown Creature",
description = "A mysterious beast of unknown origin. Its habits and abilities are yet to be fully documented. Approach with extreme caution.",
weaknesses = "Fire,Ice,Thunder",
ailments = "Poison,Sleep,Paralysis",
habitat = "Various Regions",
themeColor = "#654321", // Dark Brown
fontName = "Georgia" // Fallback font family
) {
const canvas = document.createElement('canvas');
const canvasWidth = 700;
const canvasHeight = 1000; // Fixed height, content might be clipped if too long
canvas.width = canvasWidth;
canvas.height = canvasHeight;
const ctx = canvas.getContext('2d');
// --- Helper: Font Loader ---
async function loadWebFont(fontFamilyToLoad, fontUrl) {
// Check if font is already loaded/available
if (document.fonts.check(`12px "${fontFamilyToLoad}"`)) {
// console.log(`${fontFamilyToLoad} font already available.`);
return true;
}
const fontFace = new FontFace(fontFamilyToLoad, `url(${fontUrl})`);
try {
await fontFace.load();
document.fonts.add(fontFace);
// console.log(`${fontFamilyToLoad} font loaded successfully.`);
return true;
} catch (e) {
console.error(`Font ${fontFamilyToLoad} failed to load:`, e);
return false;
}
}
// --- Helper: Text Wrapper ---
function wrapText(context, text, x, y, maxWidth, lineHeight, currentFontForWrap) {
context.font = currentFontForWrap;
let words = text.split(' ');
let line = '';
let currentLineY = y;
for (let n = 0; n < words.length; n++) {
let testLine = line + words[n] + ' ';
let metrics = context.measureText(testLine);
let testWidth = metrics.width;
if (testWidth > maxWidth && n > 0) {
context.fillText(line.trim(), x, currentLineY);
line = words[n] + ' ';
currentLineY += lineHeight;
} else {
line = testLine;
}
}
context.fillText(line.trim(), x, currentLineY);
return currentLineY + lineHeight; // Return Y position after the last line of text
}
// --- Helper: Draw Item List (Weaknesses/Ailments) ---
const ELEMENT_DATA = {
"fire": { name: "Fire", color: "#FF4500" },
"water": { name: "Water", color: "#1E90FF" },
"thunder": { name: "Thunder", color: "#FFD700" },
"ice": { name: "Ice", color: "#00BFFF" },
"dragon": { name: "Dragon", color: "#8A2BE2" }
};
const AILMENT_DATA = {
"poison": { name: "Poison", color: "#9400D3" },
"sleep": { name: "Sleep", color: "#AFEEEE" },
"paralysis": { name: "Paralysis", color: "#FFA500" },
"stun": { name: "Stun", color: "#FFFFE0" }, // LightYellow, ensure text contrast
"blast": { name: "Blast", color: "#FF6347" }
};
function drawItemsList(context, itemsString, dataMap, title, x, startY, itemIconSize, itemLineHeight, spacingAfterTitle, titleFont, itemFont, listTitleColor, itemTextColor) {
context.fillStyle = listTitleColor;
context.font = titleFont;
// Measure title height for better spacing
const titleMetrics = context.measureText(title);
const titleHeight = (titleMetrics.actualBoundingBoxAscent || parseInt(titleFont.match(/\d+px/)[0])) + (titleMetrics.actualBoundingBoxDescent || 0);
context.fillText(title, x, startY + titleHeight * 0.8); // Approximate baseline adjustment
let currentItemY = startY + titleHeight + spacingAfterTitle;
const items = itemsString.toLowerCase().split(',').map(s => s.trim()).filter(s => s);
items.forEach(itemKey => {
const itemData = dataMap[itemKey.toLowerCase()]; // Ensure key is lowercase for lookup
if (itemData) {
// Draw colored square icon
context.fillStyle = itemData.color;
context.fillRect(x, currentItemY, itemIconSize, itemIconSize);
// Draw item name text
context.fillStyle = itemTextColor;
context.font = itemFont;
let textToDraw = itemData.name || (itemKey.charAt(0).toUpperCase() + itemKey.slice(1));
context.textBaseline = 'middle';
context.fillText(textToDraw, x + itemIconSize + 10, currentItemY + itemIconSize / 2);
context.textBaseline = 'alphabetic'; // Reset to default
currentItemY += itemLineHeight;
}
});
return currentItemY; // Return Y position after last item
}
// --- Content Configuration ---
const pagePadding = 30;
let currentY = pagePadding;
const contentX = pagePadding;
const contentWidth = canvasWidth - 2 * pagePadding;
const textColor = "#333333"; // Dark gray for general text
// --- Font Setup ---
const cinzelUrl = 'https://fonts.gstatic.com/s/cinzeldecorative/v19/daaCSScvJGqLYhGmdTQIHmywRcrJq8-30m0.woff2'; // Cinzel Decorative Regular
let fontLoaded = await loadWebFont('Cinzel Decorative', cinzelUrl);
const baseFontFamily = fontLoaded ? 'Cinzel Decorative' : fontName;
// --- Styles ---
const titleFont = `bold 36px "${baseFontFamily}"`;
const sectionTitleFont = `bold 20px "${baseFontFamily}"`;
const itemFont = `16px "${baseFontFamily}"`;
const descriptionFont = `15px "${baseFontFamily}"`;
const habitatFont = `italic 16px "${baseFontFamily}"`;
const habitatValueFont = `16px "${baseFontFamily}"`; // Non-italic for value
const regularTextLineHeight = 22; // Line height for description
// 1. Background
ctx.fillStyle = '#FAF0E6'; // Parchment color
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 2. Monster Name
ctx.font = titleFont;
ctx.fillStyle = themeColor;
ctx.textAlign = 'center';
// Get height of title font for accurate placement
const monsterNameMetrics = ctx.measureText(monsterName);
const monsterNameHeight = (monsterNameMetrics.actualBoundingBoxAscent || parseInt(titleFont.match(/\d+px/)[0])) + (monsterNameMetrics.actualBoundingBoxDescent || 0);
currentY += monsterNameHeight * 0.8; // Adjust Y to be baseline
ctx.fillText(monsterName, canvasWidth / 2, currentY);
ctx.textAlign = 'left'; // Reset alignment
currentY += 30; // Spacing after title
// 3. Original Image
if (originalImg && originalImg.complete && originalImg.naturalWidth !== 0) {
const maxImgDisplayWidth = contentWidth;
const maxImgDisplayHeight = 300;
let imgDispWidth, imgDispHeight;
// Scaling logic to fit image within bounds, preserving aspect ratio, and not scaling up
let scaleRatio;
if (originalImg.width > maxImgDisplayWidth || originalImg.height > maxImgDisplayHeight) {
scaleRatio = Math.min(maxImgDisplayWidth / originalImg.width, maxImgDisplayHeight / originalImg.height);
} else {
scaleRatio = 1; // Don't scale up small images
}
imgDispWidth = originalImg.width * scaleRatio;
imgDispHeight = originalImg.height * scaleRatio;
const imgX = contentX + (contentWidth - imgDispWidth) / 2;
const imgY = currentY;
// Draw image border
ctx.strokeStyle = themeColor;
ctx.lineWidth = 3;
ctx.strokeRect(imgX - 3, imgY - 3, imgDispWidth + 6, imgDispHeight + 6);
ctx.drawImage(originalImg, imgX, imgY, imgDispWidth, imgDispHeight);
currentY += imgDispHeight + 30; // Spacing after image
} else {
// Placeholder if image is not loaded or invalid
const placeholderHeight = 150;
ctx.fillStyle = '#DDDDDD';
ctx.fillRect(contentX + contentWidth/4, currentY, contentWidth/2, placeholderHeight);
ctx.fillStyle = textColor;
ctx.font = itemFont; // Use a generic font for placeholder text
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText("Image Not Available", contentX + contentWidth / 2, currentY + placeholderHeight / 2);
ctx.textAlign = 'left';
ctx.textBaseline = 'alphabetic';
currentY += placeholderHeight + 30; // Spacing after placeholder
}
// 4. Habitat
ctx.font = habitatFont;
ctx.fillStyle = themeColor;
const habitatLabel = "Habitat: ";
const habitatLabeMetrics = ctx.measureText(habitatLabel); // For positioning the value
const habitatLineHeight = (ctx.measureText("M").actualBoundingBoxAscent || parseInt(habitatFont.match(/\d+px/)[0])) + (ctx.measureText("M").actualBoundingBoxDescent || 0);
ctx.fillText(habitatLabel, contentX, currentY + habitatLineHeight * 0.8);
ctx.font = habitatValueFont;
ctx.fillStyle = textColor;
ctx.fillText(habitat, contentX + habitatLabeMetrics.width, currentY + habitatLineHeight * 0.8);
currentY += habitatLineHeight + 20; // Spacing after habitat
// 5. Separator Line
function drawSeparator(yPos) {
ctx.beginPath();
ctx.moveTo(contentX, yPos);
ctx.lineTo(contentX + contentWidth, yPos);
ctx.strokeStyle = themeColor;
ctx.lineWidth = 1.5;
ctx.stroke();
return yPos + 20; // Spacing after separator
}
currentY = drawSeparator(currentY);
// 6. Weaknesses
const itemIconRenderSize = 20;
const itemLineRenderHeight = 28; // Icon height + vertical spacing between items
const listTitleSpacing = 10; // Space between list title and first item
currentY = drawItemsList(ctx, weaknesses, ELEMENT_DATA, "Elemental Weaknesses:",
contentX, currentY, itemIconRenderSize, itemLineRenderHeight, listTitleSpacing,
sectionTitleFont, itemFont, themeColor, textColor);
currentY += 15; // Extra spacing after the list of weaknesses
// 7. Ailments
currentY = drawItemsList(ctx, ailments, AILMENT_DATA, "Ailment Vulnerabilities:",
contentX, currentY, itemIconRenderSize, itemLineRenderHeight, listTitleSpacing,
sectionTitleFont, itemFont, themeColor, textColor);
currentY += 20; // Spacing after ailment list
// 8. Separator Line
currentY = drawSeparator(currentY);
// 9. Description
ctx.font = sectionTitleFont;
ctx.fillStyle = themeColor;
const descTitleMetrics = ctx.measureText("Description:");
const descTitleHeight = (descTitleMetrics.actualBoundingBoxAscent || parseInt(sectionTitleFont.match(/\d+px/)[0])) + (descTitleMetrics.actualBoundingBoxDescent || 0);
ctx.fillText("Description:", contentX, currentY + descTitleHeight * 0.8);
currentY += descTitleHeight + 10; // Spacing after title
ctx.fillStyle = textColor;
currentY = wrapText(ctx, description, contentX, currentY, contentWidth, regularTextLineHeight, descriptionFont);
// currentY already updated by wrapText to be below the description
return canvas;
}
Apply Changes