You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
cardName = "Mystic Automaton",
manaCost = "3U",
cardType = "Artifact Creature - Construct",
cardText = "When Mystic Automaton enters the battlefield, draw a card.\n{1}{U}: Scry 1.\n\n\"It sees possibilities hidden to mortal eyes.\"",
powerToughness = "2/3",
colorIdentity = "M", // W, U, B, R, G, M (multicolor/gold), A (artifact), L (land), C (colorless)
artistName = "AI Artist"
) {
// --- Font Loading ---
// Ensure fonts are loaded. Creates a <link> in <head> if not already present.
// This is a simplified approach. In a robust app, you'd manage font loading state more carefully.
if (!document.getElementById('mtg-card-fonts-stylesheet')) {
const fontLink = document.createElement('link');
fontLink.id = 'mtg-card-fonts-stylesheet';
fontLink.rel = 'stylesheet';
fontLink.href = 'https://fonts.googleapis.com/css2?family=IM+Fell+English+SC&family=Lora:ital,wght@0,400;0,700;1,400;1,700&family=Roboto:wght@700&display=swap';
const p = new Promise((resolve) => {
fontLink.onload = async () => {
try {
// Wait for specific fonts to be usable
await Promise.all([
document.fonts.load('1em "IM Fell English SC"'),
document.fonts.load('1em "Lora"'),
document.fonts.load('bold 1em "Roboto"')
]);
} catch (e) {
console.warn("Failed to explicitly load fonts via document.fonts.load, will rely on CSS: ", e);
}
resolve();
};
fontLink.onerror = () => {
console.error("Failed to load Google Fonts CSS for MTG Card.");
resolve(); // Resolve anyway to proceed with fallback fonts
};
});
document.head.appendChild(fontLink);
await p;
}
const titleFont = '19px "IM Fell English SC", "Times New Roman", serif';
const typeFont = '15px "IM Fell English SC", "Times New Roman", serif';
const textFont = '12px "Lora", "Georgia", serif';
const textFontItalic = 'italic 12px "Lora", "Georgia", serif';
const artistFont = '10px "Lora", "Georgia", serif';
const manaSymbolTextFont = 'bold 11px "Roboto", "Arial", sans-serif';
const ptFont = 'bold 17px "IM Fell English SC", "Times New Roman", serif';
// --- Canvas Setup ---
const cardWidth = 375;
const cardHeight = 525;
const canvas = document.createElement('canvas');
canvas.width = cardWidth;
canvas.height = cardHeight;
const ctx = canvas.getContext('2d');
// --- Helper: Rounded Rectangle ---
function drawRoundedRect(ctx, x, y, width, height, radius) {
if (typeof radius === 'number') {
radius = { tl: radius, tr: radius, br: radius, bl: radius };
} else {
const defaultRadius = { tl: 0, tr: 0, br: 0, bl: 0 };
for (let side in defaultRadius) {
radius[side] = radius[side] || defaultRadius[side];
}
}
ctx.beginPath();
ctx.moveTo(x + radius.tl, y);
ctx.lineTo(x + width - radius.tr, y);
ctx.quadraticCurveTo(x + width, y, x + width, y + radius.tr);
ctx.lineTo(x + width, y + height - radius.br);
ctx.quadraticCurveTo(x + width, y + height, x + width - radius.br, y + height);
ctx.lineTo(x + radius.bl, y + height);
ctx.quadraticCurveTo(x, y + height, x, y + height - radius.bl);
ctx.lineTo(x, y + radius.tl);
ctx.quadraticCurveTo(x, y, x + radius.tl, y);
ctx.closePath();
}
// --- Helper: Get Frame Colors ---
function getFrameColors(id) {
const sanitizedId = id.toUpperCase().replace(/[^WUBRGALC]/g, '');
let type = "GOLD"; // Default
if (sanitizedId.length === 1) {
if (sanitizedId === "W") type = "WHITE";
else if (sanitizedId === "U") type = "BLUE";
else if (sanitizedId === "B") type = "BLACK";
else if (sanitizedId === "R") type = "RED";
else if (sanitizedId === "G") type = "GREEN";
else if (sanitizedId === "A") type = "ARTIFACT";
else if (sanitizedId === "L") type = "LAND";
else if (sanitizedId === "C") type = "COLORLESS";
} else if (sanitizedId.length > 1) {
if (sanitizedId.includes("A")) type = "ARTIFACT"; // Prioritize artifact if 'A' is present
else if (sanitizedId.includes("L")) type = "LAND"; // Prioritize land if 'L' is present
else if (/[WUBRG]/.test(sanitizedId)) type = "GOLD"; // Multicolor
else type = "COLORLESS"; // If multiple but no colors (e.g. "CC")
} else { // Empty or invalid
type = "GOLD";
}
const palettes = {
TEXT_DARK: "#201008", TEXT_LIGHT: "#F0F0E0",
WHITE: {
name: "White", frameGradient: ["#F8F6D8", "#F4F2B6", "#EFECC2"], barGradient: ["#DMD8B8", "#C4BF9E", "#AEAA86"],
barTextColor: "#3A3028", textBoxBG: "rgba(245, 245, 230, 0.9)", textBoxTextColor: "#3A3028",
ptBoxBG: "#DMD8B8", ptBoxTextColor: "#3A3028", outerBorderColor: "#6D614A", setSymbolFill: "#D0C8B0", setSymbolStroke: "#444"
},
BLUE: {
name: "Blue", frameGradient: ["#AAE0FA", "#72C2F0", "#4AAADE"], barGradient: ["#0D7EBE", "#0A6093", "#084E77"],
barTextColor: "#F0F0F0", textBoxBG: "rgba(172, 203, 230, 0.9)", textBoxTextColor: "#102030",
ptBoxBG: "#0D7EBE", ptBoxTextColor: "#F0F0F0", outerBorderColor: "#083A63", setSymbolFill: "#A0C8E0", setSymbolStroke: "#FFF"
},
BLACK: {
name: "Black", frameGradient: ["#8C827B", "#6E6660", "#504A46"], barGradient: ["#4C423D", "#38312E", "#2A2421"],
barTextColor: "#F0E8E0", textBoxBG: "rgba(100, 90, 85, 0.9)", textBoxTextColor: "#F0E8E0",
ptBoxBG: "#4C423D", ptBoxTextColor: "#F0E8E0", outerBorderColor: "#1A1310", setSymbolFill: "#777", setSymbolStroke: "#222"
},
RED: {
name: "Red", frameGradient: ["#FAA8A0", "#F57E72", "#F05F50"], barGradient: ["#D3202A", "#A91820", "#88131A"],
barTextColor: "#F8F0E0", textBoxBG: "rgba(240, 180, 170, 0.9)", textBoxTextColor: "#301008",
ptBoxBG: "#D3202A", ptBoxTextColor: "#F8F0E0", outerBorderColor: "#601010", setSymbolFill: "#E8A098", setSymbolStroke: "#FFF"
},
GREEN: {
name: "Green", frameGradient: ["#9FD8A0", "#6BB06E", "#47984C"], barGradient: ["#00783C", "#005C2D", "#004020"],
barTextColor: "#F0F0F0", textBoxBG: "rgba(160, 200, 160, 0.9)", textBoxTextColor: "#082010",
ptBoxBG: "#00783C", ptBoxTextColor: "#F0F0F0", outerBorderColor: "#003018", setSymbolFill: "#A0D0A0", setSymbolStroke: "#FFF"
},
GOLD: {
name: "Gold", frameGradient: ["#D4AF37", "#C09B2D", "#AB8723"], barGradient: ["#B08D57", "#92703F", "#7A5C34"],
barTextColor: "#1A1008", textBoxBG: "rgba(240, 225, 190, 0.9)", textBoxTextColor: "#201808",
ptBoxBG: "#B08D57", ptBoxTextColor: "#1A1008", outerBorderColor: "#5C4033", setSymbolFill: "#D0B070", setSymbolStroke: "#000"
},
ARTIFACT: {
name: "Artifact", frameGradient: ["#B8B8B8", "#9C9C9C", "#828282"], barGradient: ["#707070", "#5A5A5A", "#484848"],
barTextColor: "#111111", textBoxBG: "rgba(180, 180, 180, 0.9)", textBoxTextColor: "#111111",
ptBoxBG: "#707070", ptBoxTextColor: "#111111", outerBorderColor: "#333333", setSymbolFill: "#999", setSymbolStroke: "#222"
},
LAND: {
name: "Land", frameGradient: ["#D8C8B0", "#B09A78", "#907E60"], barGradient: ["#8A7050", "#6D5840", "#544430"],
barTextColor: "#100804", textBoxBG: "rgba(210, 190, 160, 0.9)", textBoxTextColor: "#201008",
ptBoxBG: "#8A7050", ptBoxTextColor: "#100804", outerBorderColor: "#403020", setSymbolFill: "#B0A088", setSymbolStroke: "#000"
},
COLORLESS: { // For cards like Eldrazi that are not artifacts but colorless
name: "Colorless", frameGradient: ["#D1CDC9", "#B9B4B0", "#A29D98"], barGradient: ["#8B8580", "#746F6A", "#605B57"],
barTextColor: "#201810", textBoxBG: "rgba(200, 195, 190, 0.9)", textBoxTextColor: "#201810",
ptBoxBG: "#8B8580", ptBoxTextColor: "#201810", outerBorderColor: "#433E3A", setSymbolFill: "#AAA", setSymbolStroke: "#333"
}
};
return palettes[type] || palettes.GOLD;
}
const palette = getFrameColors(colorIdentity);
// --- Helper: Draw Mana Symbols ---
function drawManaSymbols(ctx, costStr, startX, startY, symbolSize, spacing) {
const symbols = [];
let current = "";
// Basic parser: "2WU" -> ["2", "W", "U"], "{X}{R}" -> ["X", "R"]
for (let i = 0; i < costStr.length; i++) {
let char = costStr[i];
if (char === '{') {
if (current) symbols.push(current); current = ""; // Push existing number
} else if (char === '}') {
if (current) symbols.push(current.toUpperCase()); current = "";
} else if (char.match(/[WUBRGXCSTPQ]|\d/i)) { // WUBRG, X, Colorless (C), Snow (S), Tap (T), Phyrexian (P, only letter)
if (char.match(/\d/) && current.match(/[A-Z]/i)) { // Number after letter means new symbol
symbols.push(current); current = "";
}
current += char;
} else { // If space or other, push current symbol
if (current) symbols.push(current.toUpperCase()); current = "";
}
}
if (current) symbols.push(current.toUpperCase());
let currentX = startX;
const radius = symbolSize / 2;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
for (const symbol of symbols) {
let bgColor, fgColor, textSymbol = symbol;
switch (symbol) {
case "W": bgColor = "#F8F7F0"; fgColor = "#231F20"; break;
case "U": bgColor = "#0E69AB"; fgColor = "#CFDDF6"; break;
case "B": bgColor = "#231F20"; fgColor = "#A5A19F"; break;
case "R": bgColor = "#D3202A"; fgColor = "#FBC5C9"; break;
case "G": bgColor = "#00733E"; fgColor = "#C4D5CA"; break;
case "C": bgColor = "#CEC8C2"; fgColor = "#231F20"; break; // Diamond for colorless
case "X": bgColor = "#CEC8C2"; fgColor = "#231F20"; break;
case "S": bgColor = "#A0D8F0"; fgColor = "#0E69AB"; textSymbol= "❄"; break; // Snow
case "T": bgColor = "#B09A78"; fgColor = "#231F20"; textSymbol= "↷"; break; // Tap
// Simplified Phyrexian: just the letter with a grey circle like numeric
case "P/W": case "WP": bgColor = "#F8F7F0"; fgColor = "#7C754D"; textSymbol= "Φ"; break;
case "P/U": case "UP": bgColor = "#0E69AB"; fgColor = "#7EAAC0"; textSymbol= "Φ"; break;
case "P/B": case "BP": bgColor = "#231F20"; fgColor = "#8E8886"; textSymbol= "Φ"; break;
case "P/R": case "RP": bgColor = "#D3202A"; fgColor = "#DC8F94"; textSymbol= "Φ"; break;
case "P/G": case "GP": bgColor = "#00733E"; fgColor = "#85B299"; textSymbol= "Φ"; break;
default: // Numeric or unknown - treat as generic cost
bgColor = "#CEC8C2"; fgColor = "#231F20"; break;
}
ctx.font = manaSymbolTextFont; // Reset for each symbol, especially after special chars
// Draw circle
ctx.beginPath();
ctx.arc(currentX + radius, startY + radius, radius, 0, Math.PI * 2);
ctx.fillStyle = bgColor;
ctx.fill();
ctx.strokeStyle = "rgba(0,0,0,0.7)";
ctx.lineWidth = 0.5;
ctx.stroke();
// Draw text
ctx.fillStyle = fgColor;
if (textSymbol === "Φ") ctx.font = `bold ${symbolSize * 0.8}px "Times New Roman", serif`; // Phyrexian symbol font
ctx.fillText(textSymbol, currentX + radius, startY + radius +1); // +1 for better vertical centering
currentX -= (symbolSize + spacing); // Mana symbols are on the right, so decrement X
}
return currentX + symbolSize + spacing; // Return the X coord of where the first symbol started (rightmost edge)
}
// --- Helper: Draw Wrapped Text ---
function drawSmartText(ctx, textToDraw, x, y, maxWidth, lineHeight, maxHeight, flavorFont) {
const paragraphs = textToDraw.split('\n');
let currentY = y;
let isFlavor = false;
for (let i = 0; i < paragraphs.length; i++) {
const paragraph = paragraphs[i];
if (currentY + lineHeight > y + maxHeight - (lineHeight/2)) break; // Check height limit before drawing paragraph
if (paragraph.trim() === "" && i < paragraphs.length - 1 && paragraphs[i+1].trim() !== "") {
isFlavor = true; // Next non-empty paragraph is flavor text
currentY += lineHeight * 0.6; // Smaller gap for flavor text
continue;
}
ctx.font = isFlavor ? flavorFont : textFont;
let words = paragraph.split(' ');
let line = '';
for (let n = 0; n < words.length; n++) {
let testLine = line + words[n] + ' ';
let metrics = ctx.measureText(testLine);
if (metrics.width > maxWidth && n > 0) {
if (currentY + lineHeight > y + maxHeight) break;
ctx.fillText(line.trim(), x, currentY);
currentY += lineHeight;
line = words[n] + ' ';
} else {
line = testLine;
}
}
if (currentY + lineHeight <= y + maxHeight + (lineHeight/2)) { // check before final line of paragraph
ctx.fillText(line.trim(), x, currentY);
} else { break; }
currentY += lineHeight;
if (isFlavor) currentY += lineHeight * 0.1; // Slightly more spacing after flavor lines
}
return currentY; // Return the Y where drawing stopped
}
// --- Layout Constants ---
const outerMargin = 3;
const innerMargin = 12; // Margin from black border to colored frame
const totalBorder = outerMargin + innerMargin; // around elements like art box
const titleBarHeight = 30;
const titleBarY = outerMargin + innerMargin / 2.5;
const titleBarPinchedWidth = 20; // How much the sides of title/type bar pinch in
const artBoxX = totalBorder + 5;
const artBoxY = titleBarY + titleBarHeight + 3;
const artBoxWidth = cardWidth - 2 * artBoxX;
const artBoxHeight = Math.floor(artBoxWidth * 0.75); // Common art aspect ratio
const typeLineHeight = 25;
const typeLineY = artBoxY + artBoxHeight + 3;
const textBoxX = artBoxX;
const textBoxY = typeLineY + typeLineHeight + 5;
const textBoxWidth = artBoxWidth;
const ptBoxWidth = 50;
const ptBoxHeight = 25;
const ptBoxPadding = 5;
const ptBoxX = cardWidth - totalBorder - ptBoxWidth - ptBoxPadding;
const ptBoxY = cardHeight - totalBorder - ptBoxHeight - ptBoxPadding;
const textBoxHeight = ptBoxY - textBoxY - 18; // Space for artist name below
const artistNameY = textBoxY + textBoxHeight + 12;
const textLineHeight = 14;
// --- Drawing ---
// 1. Black outer rounded border
ctx.fillStyle = 'black';
drawRoundedRect(ctx, outerMargin, outerMargin, cardWidth - 2 * outerMargin, cardHeight - 2 * outerMargin, 10);
ctx.fill();
// 2. Main frame color (gradient fill in a slightly smaller rounded rect)
const frameGrad = ctx.createLinearGradient(0, innerMargin, 0, cardHeight - innerMargin);
palette.frameGradient.forEach((stop, index) => frameGrad.addColorStop(index / (palette.frameGradient.length -1), stop));
ctx.fillStyle = frameGrad;
drawRoundedRect(ctx, innerMargin, innerMargin, cardWidth - 2 * innerMargin, cardHeight - 2 * innerMargin, 8);
ctx.fill();
// Common function for title/type bar shapes
function drawRuleBar(yPos, height, isTitleBar) {
const barGrad = ctx.createLinearGradient(0, yPos, 0, yPos + height);
palette.barGradient.forEach((stop, index) => barGrad.addColorStop(index / (palette.barGradient.length -1), stop));
ctx.fillStyle = barGrad;
ctx.beginPath();
ctx.moveTo(innerMargin + (isTitleBar ? 0 : titleBarPinchedWidth /2) , yPos);
ctx.lineTo(cardWidth - innerMargin - (isTitleBar ? 0 : titleBarPinchedWidth/2), yPos);
ctx.lineTo(cardWidth - innerMargin - (isTitleBar ? titleBarPinchedWidth : titleBarPinchedWidth) , yPos + height);
ctx.lineTo(innerMargin + (isTitleBar ? titleBarPinchedWidth : titleBarPinchedWidth) , yPos + height);
ctx.closePath();
ctx.fill();
// Thin border for separation
ctx.strokeStyle = palette.outerBorderColor;
ctx.lineWidth = 0.5;
ctx.stroke();
}
// 3. Title bar background
drawRuleBar(titleBarY, titleBarHeight, true);
// 4. Type line background
drawRuleBar(typeLineY, typeLineHeight, false);
// 5. Text box background
ctx.fillStyle = palette.textBoxBG;
drawRoundedRect(ctx, textBoxX - 2, textBoxY - 2, textBoxWidth + 4, textBoxHeight + artistNameY - textBoxY -5, 3); // Add little extra border
ctx.fill();
// Inner border for text box
ctx.strokeStyle = palette.outerBorderColor;
ctx.lineWidth = 0.5;
drawRoundedRect(ctx, textBoxX -2 , textBoxY -2, textBoxWidth+4, textBoxHeight + artistNameY - textBoxY -5, 3);
ctx.stroke();
// 6. Art image
if (originalImg && originalImg.complete && originalImg.naturalWidth > 0) {
const imgAspect = originalImg.naturalWidth / originalImg.naturalHeight;
const boxAspect = artBoxWidth / artBoxHeight;
let sx=0, sy=0, sWidth=originalImg.naturalWidth, sHeight=originalImg.naturalHeight;
if (imgAspect > boxAspect) { // Image wider than box: crop sides
sWidth = originalImg.naturalHeight * boxAspect;
sx = (originalImg.naturalWidth - sWidth) / 2;
} else { // Image taller than box: crop top/bottom
sHeight = originalImg.naturalWidth / boxAspect;
sy = (originalImg.naturalHeight - sHeight) / 2;
}
ctx.drawImage(originalImg, sx, sy, sWidth, sHeight, artBoxX, artBoxY, artBoxWidth, artBoxHeight);
} else { // Placeholder if image fails
ctx.fillStyle = '#555';
ctx.fillRect(artBoxX, artBoxY, artBoxWidth, artBoxHeight);
ctx.fillStyle = 'white'; ctx.textAlign = 'center'; ctx.fillText("Image Error", artBoxX + artBoxWidth/2, artBoxY + artBoxHeight/2);
}
// 7. Border around art image
ctx.strokeStyle = palette.outerBorderColor;
ctx.lineWidth = 1;
ctx.strokeRect(artBoxX, artBoxY, artBoxWidth, artBoxHeight);
// Text drawing setup
ctx.fillStyle = palette.barTextColor;
ctx.textBaseline = 'middle';
// Shadow for title/type text
function setTextShadow(color = "rgba(0,0,0,0.5)", blur = 1, offsetX = 0.5, offsetY = 0.5) {
ctx.shadowColor = color;
ctx.shadowBlur = blur;
ctx.shadowOffsetX = offsetX;
ctx.shadowOffsetY = offsetY;
}
function clearTextShadow() {
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
}
// 8. Card Name text
ctx.font = titleFont;
ctx.textAlign = 'left';
setTextShadow(palette.name === "Black" || palette.name === "Blue" ? "rgba(0,0,0,0.7)" : "rgba(255,255,255,0.4)");
ctx.fillText(cardName, innerMargin + titleBarPinchedWidth + 5, titleBarY + titleBarHeight / 2 + 1);
clearTextShadow();
// 9. Mana Symbols
// Mana symbols are drawn from right to left. StartX is the rightmost edge of the mana cost area.
const manaSymbolSize = 15;
const manaSymbolSpacing = -2; // Negative for slight overlap
const manaCostRightMargin = 5;
drawManaSymbols(ctx, manaCost, cardWidth - innerMargin - manaCostRightMargin - manaSymbolSize, titleBarY + (titleBarHeight - manaSymbolSize) / 2, manaSymbolSize, manaSymbolSpacing);
// 10. Card Type text
ctx.font = typeFont;
ctx.textAlign = 'left';
setTextShadow(palette.name === "Black" || palette.name === "Blue" ? "rgba(0,0,0,0.7)" : "rgba(255,255,255,0.4)");
ctx.fillText(cardType, innerMargin + titleBarPinchedWidth + 5, typeLineY + typeLineHeight / 2 + 1);
clearTextShadow();
// Set symbol placeholder
const setSymbolRadius = typeLineHeight / 3.5;
const setSymbolX = cardWidth - innerMargin - titleBarPinchedWidth/2 - setSymbolRadius - 10;
const setSymbolY = typeLineY + typeLineHeight / 2;
ctx.beginPath();
ctx.arc(setSymbolX, setSymbolY, setSymbolRadius, 0, Math.PI * 2);
ctx.fillStyle = palette.setSymbolFill;
ctx.fill();
ctx.strokeStyle = palette.setSymbolStroke;
ctx.lineWidth = 1;
ctx.stroke();
// 11. Main Card Text
ctx.fillStyle = palette.textBoxTextColor;
ctx.textAlign = 'left';
ctx.textBaseline = 'top'; // Important for wrapped text
drawSmartText(ctx, cardText, textBoxX + 5, textBoxY + 5, textBoxWidth - 10, textLineHeight, textBoxHeight - 5, textFontItalic);
// 12. Artist Name text
ctx.font = artistFont;
ctx.fillStyle = palette.textBoxTextColor; // May differ based on palette
ctx.textAlign = 'left';
ctx.textBaseline = 'bottom';
ctx.fillText(`Illus. ${artistName}`, textBoxX + 5, artistNameY -1); // -1 to pull up a bit from very bottom
// 13. P/T Box & Text (if P/T provided)
if (powerToughness && powerToughness.trim() !== "") {
// Background
ctx.fillStyle = palette.ptBoxBG;
// PT Box shape is specific. Simplified: rounded rect
drawRoundedRect(ctx, ptBoxX, ptBoxY, ptBoxWidth, ptBoxHeight, 5);
ctx.fill();
ctx.strokeStyle = palette.outerBorderColor;
ctx.lineWidth = 1;
ctx.stroke();
// Text
ctx.font = ptFont;
ctx.fillStyle = palette.ptBoxTextColor;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
setTextShadow(palette.name === "Black" || palette.name === "Blue" || palette.name === "Green" || palette.name === "Red" ? "rgba(0,0,0,0.6)" : "rgba(255,255,255,0.5)");
ctx.fillText(powerToughness, ptBoxX + ptBoxWidth / 2, ptBoxY + ptBoxHeight / 2 + 1);
clearTextShadow();
}
return canvas;
}
Apply Changes