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!
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.