You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, heroName = "Сэр Ланселот", kingdomName = "Авалон") {
// Dynamically load a medieval Cyrillic-supporting Google Font
const fontName = "Cormorant Garamond";
const fontURL = `https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@600;700&display=swap`;
if (!document.querySelector(`link[href="${fontURL}"]`)) {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = fontURL;
document.head.appendChild(link);
}
// Attempt to wait for the font to load, fallback safely if it fails
try {
await document.fonts.ready;
await document.fonts.load(`700 36px "${fontName}"`);
await document.fonts.load(`600 24px "${fontName}"`);
} catch (e) {
console.warn("Custom font loading failed, using fallback web-safe fonts.", e);
}
// Set up canvas dimensions (Landscape Story Book style)
const canvas = document.createElement("canvas");
canvas.width = 850;
canvas.height = 550;
const ctx = canvas.getContext("2d");
// === 1. Draw Old Parchment Background ===
// Base parchment color
ctx.fillStyle = "#ebddc0";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Apply radial gradient vignette to look like aged paper
const grad = ctx.createRadialGradient(
canvas.width / 2, canvas.height / 2, 80,
canvas.width / 2, canvas.height / 2, Math.max(canvas.width, canvas.height)
);
grad.addColorStop(0, "rgba(0,0,0,0)");
grad.addColorStop(1, "rgba(120, 80, 50, 0.6)");
ctx.fillStyle = grad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw ornate decorative borders
ctx.strokeStyle = "#5a3d1c";
// Outer thick line
ctx.lineWidth = 4;
ctx.strokeRect(20, 20, canvas.width - 40, canvas.height - 40);
// Inner thin lines
ctx.lineWidth = 1;
ctx.strokeRect(28, 28, canvas.width - 56, canvas.height - 56);
ctx.strokeRect(12, 12, canvas.width - 24, canvas.height - 24);
// === 2. Draw Knight's Portrait (the provided image) ===
const imgX = 50;
const imgY = 70;
const imgW = 320;
const imgH = 410;
// Dark underlay for image
ctx.fillStyle = "#111";
ctx.fillRect(imgX, imgY, imgW, imgH);
// Calculate 'object-fit: cover' behavior for canvas
const imgRatio = originalImg.width / originalImg.height;
const boxRatio = imgW / imgH;
let sX = 0, sY = 0, sW = originalImg.width, sH = originalImg.height;
if (imgRatio > boxRatio) {
sW = originalImg.height * boxRatio;
sX = (originalImg.width - sW) / 2;
} else {
sH = originalImg.width / boxRatio;
sY = (originalImg.height - sH) / 2;
}
// Draw the actual image
ctx.drawImage(originalImg, sX, sY, sW, sH, imgX, imgY, imgW, imgH);
// Draw frame around the knight's portrait
ctx.strokeStyle = "#b89947"; // Golden stroke
ctx.lineWidth = 4;
ctx.strokeRect(imgX, imgY, imgW, imgH);
ctx.strokeStyle = "#3d250c"; // Dark iron stroke
ctx.lineWidth = 2;
ctx.strokeRect(imgX - 4, imgY - 4, imgW + 8, imgH + 8);
ctx.strokeRect(imgX + 4, imgY + 4, imgW - 8, imgH - 8);
// === 3. Generate Knight Story Text ===
const textX = 410;
const maxTextWidth = 390;
const centerRight = textX + maxTextWidth / 2;
// Story Title
ctx.fillStyle = "#2b1b0b";
ctx.font = `700 36px "${fontName}", Georgia, serif`;
ctx.textAlign = "center";
ctx.textBaseline = "top";
ctx.fillText("История рыцаря", centerRight, 70);
// Title Decoration (Line and Diamond)
ctx.strokeStyle = "#5a3d1c";
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(centerRight - 120, 125);
ctx.lineTo(centerRight + 120, 125);
ctx.stroke();
ctx.fillStyle = "#5a3d1c";
ctx.beginPath();
ctx.moveTo(centerRight, 120);
ctx.lineTo(centerRight + 8, 125);
ctx.lineTo(centerRight, 130);
ctx.lineTo(centerRight - 8, 125);
ctx.fill();
// Story Content
ctx.font = `600 24px "${fontName}", Georgia, serif`;
ctx.textAlign = "left";
const storyText = `В славном королевстве ${kingdomName} из поколения в поколение передавалась легенда о величайшем герое. Имя ему было ${heroName}. Говорят, его доспехи сияли ярче полуденного солнца, а меч рассекал даже драконью чешую. Перед вами подлинный портрет рыцаря, чудом сохранившийся в архивах старого замка. Пусть его подвиги живут в веках, вдохновляя новые поколения на великие свершения!`;
// Routine to word-wrap text
let textY = 160;
const words = storyText.split(" ");
let line = "";
const lineHeight = 34;
for (let n = 0; n < words.length; n++) {
const testLine = line + words[n] + " ";
const metrics = ctx.measureText(testLine);
if (metrics.width > maxTextWidth && n > 0) {
ctx.fillText(line, textX, textY);
line = words[n] + " ";
textY += lineHeight;
} else {
line = testLine;
}
}
ctx.fillText(line, textX, textY);
// === 4. Add a Medieval Wax Seal ===
const sealX = canvas.width - 65;
const sealY = canvas.height - 65;
// Outer jagged wax
ctx.fillStyle = "#8a1c1c";
ctx.beginPath();
for (let i = 0; i < 20; i++) {
const angle = (i / 20) * Math.PI * 2;
const radius = i % 2 === 0 ? 30 : 25;
if (i === 0) ctx.moveTo(sealX + Math.cos(angle) * radius, sealY + Math.sin(angle) * radius);
else ctx.lineTo(sealX + Math.cos(angle) * radius, sealY + Math.sin(angle) * radius);
}
ctx.closePath();
ctx.fill();
// Inner smooth seal depression
ctx.fillStyle = "#5c1010";
ctx.beginPath();
ctx.arc(sealX, sealY, 20, 0, Math.PI * 2);
ctx.fill();
// Seal Emblem (Letter)
ctx.fillStyle = "#d99c9c";
ctx.font = `700 24px "${fontName}", Georgia, serif`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
// Using the first letter of the hero's name
ctx.fillText(heroName.charAt(0).toUpperCase() || "R", sealX, sealY + 2);
return canvas;
}
Apply Changes