You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
cardName = "Card Name",
cardType = "Effect Monster",
attribute = "DARK",
levelRankLinkVal = "7", // For Monsters: Level/Rank. For Link: "L-3". For Spell/Trap: N/A.
attack = "2500", // For Spell/Trap: N/A.
defOrLinkMarkers = "2100", // For Monsters: DEF. For Link: "T,B,BL,BR". For Spell/Trap: N/A.
typeLine = "[Warrior / Effect]", // E.g., "[Dragon/Fusion/Effect]", "[Spell Card]", "[Trap Card / Counter]"
cardDescription = "Card effect text goes here. It can be quite long and should wrap lines appropriately.",
serial = "XXXX-EN000",
pendulumScale = "", // E.g., "4". If non-empty, it's a Pendulum card.
pendulumEffect = "" // Pendulum effect text, if it's a Pendulum card.
) {
const canvas = document.createElement('canvas');
const CARD_WIDTH = 420; // Slightly wider to match proportions better
const CARD_HEIGHT = 610; // 59mm x 86mm ratio is ~1.457. 420 * 1.457 = ~612
canvas.width = CARD_WIDTH;
canvas.height = CARD_HEIGHT;
const ctx = canvas.getContext('2d');
// --- Determine if it's a Pendulum card ---
const isPendulum = pendulumScale !== "" && pendulumScale !== null;
// --- Define Colors ---
const COLORS = {
"Normal Monster": "#FDE68A",
"Effect Monster": "#FF8B53",
"Ritual Monster": "#9DB5CC",
"Fusion Monster": "#A086B7",
"Synchro Monster": "#CFCFCF",
"Xyz Monster": "#282828",
"Link Monster": "#0070DD",
"Spell Card": "#1D9E74",
"Trap Card": "#BC5A84",
"Token": "#DADADA",
"Skill Card": "#82B9ED",
// Pendulum monsters use their monster type color for the top half
"Pendulum Normal Monster": "#FDE68A",
"Pendulum Effect Monster": "#FF8B53",
"Pendulum Ritual Monster": "#9DB5CC",
"Pendulum Fusion Monster": "#A086B7",
"Pendulum Synchro Monster": "#CFCFCF",
"Pendulum Xyz Monster": "#282828",
"NameTextLight": "#FFFFFF",
"NameTextDark": "#000000",
"AttributeTextLight": "#000000", // Text on attribute symbol
"AttributeTextDark": "#FFFFFF", // For dark attributes
"AttributeDARK": "#505050", "AttributeLIGHT": "#F0E68C",
"AttributeEARTH": "#8B4513", "AttributeWATER": "#4682B4",
"AttributeFIRE": "#FF4500", "AttributeWIND": "#3CB371",
"AttributeDIVINE": "#FFD700", "AttributeSPELLICON": "#1D9E74", // Using icon background as main color
"AttributeTRAPICON": "#BC5A84",
"LevelStar": "#FFD700",
"RankStar": "#A0A0A0", // Xyz Ranks are often depicted differently or on black stars
"LinkArrow": "#FF0000",
"PendulumSpellHalfBG": "#E0F0E0", // Light greenish for the Pendulum spell text area
"PendulumScaleBox": "#ADD8E6", // Light blue for scale boxes
"Border": "#1E1E1E",
"TextBoxInnerBG_Light": "#EFEFEF",
"TextBoxInnerBG_Dark": "#D0D0D0", // For Xyz mainly
};
// --- Layout Constants ---
const BORDER_WIDTH = 4; // Outer border
const INNER_PADDING = 18; // Padding from outer border to content area
const NAME_BAR_X = INNER_PADDING;
const NAME_BAR_Y = INNER_PADDING;
const NAME_BAR_HEIGHT = 48;
const NAME_FONT_SIZE = 28;
const NAME_X_OFFSET = NAME_BAR_X + 10;
const NAME_Y_OFFSET = NAME_BAR_Y + NAME_BAR_HEIGHT / 2 + 8;
const ATTRIBUTE_AREA_X = CARD_WIDTH - INNER_PADDING - 40;
const ATTRIBUTE_AREA_Y = NAME_BAR_Y + 5;
const ATTRIBUTE_SIZE = 38;
let ART_IMAGE_X = INNER_PADDING + (isPendulum ? 0 : 20);
let ART_IMAGE_Y = NAME_BAR_Y + NAME_BAR_HEIGHT + (isPendulum ? 35 : 10); // Lowered for Pendulum scales
let ART_IMAGE_WIDTH = CARD_WIDTH - 2 * ART_IMAGE_X;
let ART_IMAGE_HEIGHT = ART_IMAGE_WIDTH * (isPendulum ? 0.75 : 1); // Pend Art is wider/shorter
const LEVEL_STAR_SIZE = 18;
const LEVEL_STAR_GAP = 3;
const LEVEL_BAR_Y = NAME_BAR_Y + NAME_BAR_HEIGHT - (LEVEL_STAR_SIZE) + 5; // Above artwork, below name for non-Xyz
const MONSTER_STATS_Y = CARD_HEIGHT - INNER_PADDING - 45;
const TYPE_LINE_Y = ART_IMAGE_Y + ART_IMAGE_HEIGHT + 25;
const TYPE_LINE_HEIGHT = 25;
const TYPE_LINE_FONT_SIZE = 14;
let DESC_BOX_X = ART_IMAGE_X;
let DESC_BOX_Y = TYPE_LINE_Y + TYPE_LINE_HEIGHT + 5;
let DESC_BOX_WIDTH = ART_IMAGE_WIDTH;
let DESC_BOX_HEIGHT = MONSTER_STATS_Y - DESC_BOX_Y - 10;
const TEXT_FONT_SIZE = 12.5;
const TEXT_LINE_HEIGHT = TEXT_FONT_SIZE * 1.2;
const ATK_DEF_TEXT_Y = CARD_HEIGHT - INNER_PADDING - 18;
const ATK_TEXT_X = CARD_WIDTH / 2 + 60;
const DEF_TEXT_X = CARD_WIDTH - INNER_PADDING - 65;
const SERIAL_TEXT_Y = ATK_DEF_TEXT_Y;
const SERIAL_TEXT_X = INNER_PADDING + 5;
const COPYRIGHT_TEXT_Y = CARD_HEIGHT - INNER_PADDING + 8;
// Pendulum specific adjustments
const PENDULUM_SCALE_BOX_WIDTH = 50;
const PENDULUM_SCALE_BOX_HEIGHT = 40;
const PENDULUM_BLUE_SCALE_X = INNER_PADDING + 5;
const PENDULUM_RED_SCALE_X = CARD_WIDTH - INNER_PADDING - PENDULUM_SCALE_BOX_WIDTH - 5;
const PENDULUM_SCALE_Y = NAME_BAR_Y + NAME_BAR_HEIGHT + 5;
const PENDULUM_TEXT_FONT_SIZE = 11;
const PENDULUM_EFFECT_BOX_HEIGHT = 80;
if (isPendulum) {
ART_IMAGE_Y = PENDULUM_SCALE_Y + PENDULUM_SCALE_BOX_HEIGHT + 5;
ART_IMAGE_HEIGHT = ART_IMAGE_WIDTH * 0.6; // Adjust art height for pendulum
// Monster effect box is smaller and above pendulum effect box
const monsterEffectBoxHeight = DESC_BOX_HEIGHT - PENDULUM_EFFECT_BOX_HEIGHT - 15;
DESC_BOX_HEIGHT = monsterEffectBoxHeight;
}
// --- Determine colors based on cardType ---
let frameColor = COLORS[cardType] || COLORS["Effect Monster"];
ctx.fillStyle = frameColor;
ctx.fillRect(0, 0, CARD_WIDTH, CARD_HEIGHT);
// Outer border
ctx.strokeStyle = COLORS["Border"];
ctx.lineWidth = BORDER_WIDTH;
ctx.strokeRect(BORDER_WIDTH / 2, BORDER_WIDTH / 2, CARD_WIDTH - BORDER_WIDTH, CARD_HEIGHT - BORDER_WIDTH);
// Inner "content" area background (slightly lighter/different for some card types)
let contentBgColor = frameColor; // Default to same as frame
if (cardType === "Xyz Monster") contentBgColor = "#3A3A3A";
else if (cardType === "Synchro Monster") contentBgColor = "#D8D8D8";
ctx.fillStyle = contentBgColor;
if (isPendulum) {
const monsterPartHeight = DESC_BOX_Y + DESC_BOX_HEIGHT + 5;
ctx.fillRect(INNER_PADDING, INNER_PADDING, CARD_WIDTH - 2 * INNER_PADDING, monsterPartHeight - INNER_PADDING);
// Pendulum lower half (spell-like)
ctx.fillStyle = COLORS.PendulumSpellHalfBG;
ctx.fillRect(INNER_PADDING, monsterPartHeight, CARD_WIDTH - 2 * INNER_PADDING, (MONSTER_STATS_Y - 5) - monsterPartHeight);
} else {
ctx.fillRect(INNER_PADDING, INNER_PADDING, CARD_WIDTH - 2 * INNER_PADDING, CARD_HEIGHT - 2 * INNER_PADDING);
}
// Card Name
const nameIsLight = cardType === "Xyz Monster" || cardType === "Link Monster" || cardType.startsWith("Pendulum Xyz");
ctx.fillStyle = nameIsLight ? COLORS.NameTextLight : COLORS.NameTextDark;
ctx.font = `bold ${NAME_FONT_SIZE}px 'Arial Black', Gadget, sans-serif`;
ctx.textAlign = "left";
ctx.fillText(cardName.toUpperCase(), NAME_X_OFFSET, NAME_Y_OFFSET, CARD_WIDTH - INNER_PADDING - NAME_X_OFFSET - ATTRIBUTE_SIZE - 10);
// Attribute
// Use uppercase for attribute key, and specific "SPELLICON", "TRAPICON" for spell/trap
let attributeColorKey = `Attribute${attribute.toUpperCase()}`;
if (cardType === "Spell Card") attributeColorKey = "AttributeSPELLICON";
if (cardType === "Trap Card") attributeColorKey = "AttributeTRAPICON";
ctx.fillStyle = COLORS[attributeColorKey] || COLORS.AttributeDARK;
ctx.beginPath();
ctx.arc(ATTRIBUTE_AREA_X + ATTRIBUTE_SIZE / 2, ATTRIBUTE_AREA_Y + ATTRIBUTE_SIZE / 2, ATTRIBUTE_SIZE / 2, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = (attribute === "LIGHT" || attribute === "DIVINE") ? COLORS.AttributeTextLight : COLORS.AttributeTextDark;
if (cardType === "Spell Card" || cardType === "Trap Card") ctx.fillStyle = COLORS.AttributeTextDark;
ctx.font = `bold ${TEXT_FONT_SIZE * 0.9}px Arial, sans-serif`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
let attrText = attribute.substring(0,1).toUpperCase();
if (cardType === "Spell Card") attrText = "魔法"; // Kanji for "Spell"
if (cardType === "Trap Card") attrText = "罠"; // Kanji for "Trap"
ctx.fillText(attrText, ATTRIBUTE_AREA_X + ATTRIBUTE_SIZE / 2, ATTRIBUTE_AREA_Y + ATTRIBUTE_SIZE / 2 + 1);
ctx.textBaseline = "alphabetic"; // Reset
// Image
if (originalImg && originalImg.width > 0 && originalImg.height > 0) {
ctx.fillStyle = "#222"; // Placeholder for artbox background
ctx.fillRect(ART_IMAGE_X, ART_IMAGE_Y, ART_IMAGE_WIDTH, ART_IMAGE_HEIGHT);
// Draw image with object-fit: cover behavior
const artAspect = ART_IMAGE_WIDTH / ART_IMAGE_HEIGHT;
const imgAspect = originalImg.width / originalImg.height;
let sx = 0, sy = 0, sWidth = originalImg.width, sHeight = originalImg.height;
if (imgAspect > artAspect) { // Image wider than art box
sWidth = originalImg.height * artAspect;
sx = (originalImg.width - sWidth) / 2;
} else if (imgAspect < artAspect) { // Image taller than art box
sHeight = originalImg.width / artAspect;
sy = (originalImg.height - sHeight) / 2;
}
ctx.drawImage(originalImg, sx, sy, sWidth, sHeight, ART_IMAGE_X, ART_IMAGE_Y, ART_IMAGE_WIDTH, ART_IMAGE_HEIGHT);
}
// Art Box Border
ctx.strokeStyle = nameIsLight ? COLORS.NameTextLight : COLORS.NameTextDark;
ctx.lineWidth = 1.5;
ctx.strokeRect(ART_IMAGE_X, ART_IMAGE_Y, ART_IMAGE_WIDTH, ART_IMAGE_HEIGHT);
// Level/Rank Stars or Link Rating
const isMonster = !["Spell Card", "Trap Card", "Skill Card"].includes(cardType);
const isXyz = cardType === "Xyz Monster" || cardType === "Pendulum Xyz Monster";
const isLink = cardType === "Link Monster";
if (isMonster && !isLink && levelRankLinkVal && !isNaN(parseInt(levelRankLinkVal))) {
const numStars = parseInt(levelRankLinkVal);
ctx.fillStyle = isXyz ? COLORS.RankStar : COLORS.LevelStar;
const starY = isXyz ? (ART_IMAGE_Y + LEVEL_STAR_SIZE * 1.5) : LEVEL_BAR_Y;
const startX = isXyz ? (ART_IMAGE_X + LEVEL_STAR_SIZE)
: (CARD_WIDTH - INNER_PADDING - LEVEL_STAR_SIZE - ((numStars -1) * (LEVEL_STAR_SIZE + LEVEL_STAR_GAP)));
for (let i = 0; i < numStars; i++) {
const starX = isXyz ? startX + i * (LEVEL_STAR_SIZE + LEVEL_STAR_GAP)
: startX + i * (LEVEL_STAR_SIZE + LEVEL_STAR_GAP);
ctx.beginPath();
// Simple circle for stars
ctx.arc(starX, starY, LEVEL_STAR_SIZE / 2, 0, Math.PI * 2);
ctx.fill();
if (isXyz) { // Xyz stars often have inner detail or darker color
ctx.strokeStyle = "#000"; ctx.lineWidth=1; ctx.stroke();
}
}
}
// [Type Line Text] - e.g., [Warrior/Effect]
ctx.fillStyle = (isXyz || isLink) ? COLORS.NameTextLight : COLORS.NameTextDark;
ctx.font = `bold ${TYPE_LINE_FONT_SIZE}px 'Stone Serif', 'Times New Roman', serif`;
ctx.textAlign = "left";
const typeLineAreaWidth = ART_IMAGE_WIDTH;
const typeLineBoxX = ART_IMAGE_X;
if(isPendulum){
ctx.fillStyle = COLORS.NameTextDark; // Pendulum Type line Text is usually dark
}
ctx.fillText(typeLine, typeLineBoxX + 5, TYPE_LINE_Y, typeLineAreaWidth - 10);
// Description/Effect Text Box
const textBoxBgColor = (isXyz && !isPendulum) ? COLORS.TextBoxInnerBG_Dark : COLORS.TextBoxInnerBG_Light;
ctx.fillStyle = textBoxBgColor;
ctx.fillRect(DESC_BOX_X, DESC_BOX_Y, DESC_BOX_WIDTH, DESC_BOX_HEIGHT);
ctx.strokeStyle = (isXyz || isLink) ? COLORS.NameTextLight : COLORS.NameTextDark;
ctx.lineWidth = 0.5;
ctx.strokeRect(DESC_BOX_X, DESC_BOX_Y, DESC_BOX_WIDTH, DESC_BOX_HEIGHT);
ctx.fillStyle = COLORS.NameTextDark;
ctx.font = `${TEXT_FONT_SIZE}px 'Stone Serif', 'Times New Roman', serif`;
ctx.textAlign = "left";
wrapText(ctx, cardDescription, DESC_BOX_X + 5, DESC_BOX_Y + TEXT_FONT_SIZE + 2, DESC_BOX_WIDTH - 10, TEXT_LINE_HEIGHT);
// Pendulum Specific Elements
if (isPendulum) {
// Scales
ctx.fillStyle = COLORS.PendulumScaleBox;
ctx.fillRect(PENDULUM_BLUE_SCALE_X, PENDULUM_SCALE_Y, PENDULUM_SCALE_BOX_WIDTH, PENDULUM_SCALE_BOX_HEIGHT); // Left (Blue)
ctx.fillRect(PENDULUM_RED_SCALE_X, PENDULUM_SCALE_Y, PENDULUM_SCALE_BOX_WIDTH, PENDULUM_SCALE_BOX_HEIGHT); // Right (Red)
ctx.strokeStyle = COLORS.NameTextDark; ctx.lineWidth = 1;
ctx.strokeRect(PENDULUM_BLUE_SCALE_X, PENDULUM_SCALE_Y, PENDULUM_SCALE_BOX_WIDTH, PENDULUM_SCALE_BOX_HEIGHT);
ctx.strokeRect(PENDULUM_RED_SCALE_X, PENDULUM_SCALE_Y, PENDULUM_SCALE_BOX_WIDTH, PENDULUM_SCALE_BOX_HEIGHT);
ctx.fillStyle = COLORS.NameTextDark;
ctx.font = `bold ${NAME_FONT_SIZE * 0.8}px Arial, sans-serif`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(pendulumScale, PENDULUM_BLUE_SCALE_X + PENDULUM_SCALE_BOX_WIDTH / 2, PENDULUM_SCALE_Y + PENDULUM_SCALE_BOX_HEIGHT / 2);
ctx.fillText(pendulumScale, PENDULUM_RED_SCALE_X + PENDULUM_SCALE_BOX_WIDTH / 2, PENDULUM_SCALE_Y + PENDULUM_SCALE_BOX_HEIGHT / 2);
ctx.textBaseline = "alphabetic"; // Reset
// Pendulum Effect Box
const pEffectBoxY = DESC_BOX_Y + DESC_BOX_HEIGHT + 10;
ctx.fillStyle = COLORS.PendulumSpellHalfBG; // Ensure this part uses the pendulum area background
ctx.fillRect(DESC_BOX_X, pEffectBoxY, DESC_BOX_WIDTH, PENDULUM_EFFECT_BOX_HEIGHT);
ctx.strokeStyle = COLORS.NameTextDark; ctx.lineWidth = 0.5;
ctx.strokeRect(DESC_BOX_X, pEffectBoxY, DESC_BOX_WIDTH, PENDULUM_EFFECT_BOX_HEIGHT);
ctx.fillStyle = COLORS.NameTextDark;
ctx.font = `${PENDULUM_TEXT_FONT_SIZE}px 'Stone Serif', 'Times New Roman', serif`;
ctx.textAlign = "left";
wrapText(ctx, pendulumEffect, DESC_BOX_X + 5, pEffectBoxY + PENDULUM_TEXT_FONT_SIZE + 2, DESC_BOX_WIDTH - 10, PENDULUM_TEXT_FONT_SIZE * 1.2);
}
// ATK/DEF or Link Rating/Arrows
if (isMonster) {
ctx.fillStyle = (isXyz || isLink) ? COLORS.NameTextLight : COLORS.NameTextDark;
ctx.font = `bold ${TYPE_LINE_FONT_SIZE * 1.1}px Arial, sans-serif`;
ctx.textAlign = "right";
if (isLink) {
ctx.fillText(`ATK/${attack} LINK-${levelRankLinkVal.replace('L-','')}`, CARD_WIDTH - INNER_PADDING - 5, ATK_DEF_TEXT_Y);
// Draw Link Arrows
const arrowSize = 10;
const arrowOffset = 3; // Offset from edge of art box
const arrows = defOrLinkMarkers.split(',').map(a => a.trim().toUpperCase());
const positions = {
T: { x: ART_IMAGE_X + ART_IMAGE_WIDTH / 2, y: ART_IMAGE_Y - arrowOffset, w: arrowSize, h: arrowSize/2 },
B: { x: ART_IMAGE_X + ART_IMAGE_WIDTH / 2, y: ART_IMAGE_Y + ART_IMAGE_HEIGHT + arrowOffset - arrowSize/2, w: arrowSize, h: arrowSize/2 },
L: { x: ART_IMAGE_X - arrowOffset, y: ART_IMAGE_Y + ART_IMAGE_HEIGHT / 2, w: arrowSize/2, h: arrowSize },
R: { x: ART_IMAGE_X + ART_IMAGE_WIDTH + arrowOffset - arrowSize/2, y: ART_IMAGE_Y + ART_IMAGE_HEIGHT / 2, w: arrowSize/2, h: arrowSize },
TL: { x: ART_IMAGE_X - arrowOffset, y: ART_IMAGE_Y - arrowOffset, w: arrowSize * 0.7, h: arrowSize * 0.7 },
TR: { x: ART_IMAGE_X + ART_IMAGE_WIDTH + arrowOffset - arrowSize*0.7, y: ART_IMAGE_Y - arrowOffset, w: arrowSize * 0.7, h: arrowSize * 0.7 },
BL: { x: ART_IMAGE_X - arrowOffset, y: ART_IMAGE_Y + ART_IMAGE_HEIGHT + arrowOffset - arrowSize*0.7, w: arrowSize * 0.7, h: arrowSize * 0.7 },
BR: { x: ART_IMAGE_X + ART_IMAGE_WIDTH + arrowOffset - arrowSize*0.7, y: ART_IMAGE_Y + ART_IMAGE_HEIGHT + arrowOffset - arrowSize*0.7, w: arrowSize * 0.7, h: arrowSize * 0.7 },
};
ctx.fillStyle = COLORS.LinkArrow;
arrows.forEach(arrowKey => {
if (positions[arrowKey]) {
const p = positions[arrowKey];
// simplified rectangles for arrows
if (arrowKey === 'T') ctx.fillRect(p.x - p.w/2, p.y - p.h, p.w, p.h);
else if (arrowKey === 'B') ctx.fillRect(p.x - p.w/2, p.y, p.w, p.h);
else if (arrowKey === 'L') ctx.fillRect(p.x - p.w, p.y - p.h/2, p.w, p.h);
else if (arrowKey === 'R') ctx.fillRect(p.x, p.y - p.h/2, p.w, p.h);
else ctx.fillRect(p.x, p.y, p.w, p.h); // Diagonal simplified
}
});
} else { // Normal ATK/DEF
ctx.fillText(`ATK/${attack} DEF/${defOrLinkMarkers}`, CARD_WIDTH - INNER_PADDING - 5, ATK_DEF_TEXT_Y);
}
}
// Serial Number
ctx.fillStyle = (isXyz || isLink) ? COLORS.NameTextLight : COLORS.NameTextDark;
ctx.font = `${TEXT_FONT_SIZE * 0.8}px Arial, sans-serif`;
ctx.textAlign = "left";
ctx.fillText(serial, SERIAL_TEXT_X, SERIAL_TEXT_Y);
// Copyright (very small)
ctx.textAlign = "right";
ctx.font = `${TEXT_FONT_SIZE * 0.7}px Arial, sans-serif`;
ctx.fillText("©STUDIO DICE/SHUEISHA, TV TOKYO, KONAMI", CARD_WIDTH - INNER_PADDING - 5, COPYRIGHT_TEXT_Y);
// Helper function for text wrapping
function wrapText(context, text, x, y, maxWidth, lineHeight) {
const words = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split(/ |\n/); // Split by space or newline
let line = '';
let currentY = y;
let firstWordInLine = true;
for (let n = 0; n < words.length; n++) {
if (words[n] === '' && text.includes('\n')) { // Check for explicit newline from split
context.fillText(line, x, currentY);
line = '';
currentY += lineHeight;
firstWordInLine = true;
continue;
}
const testWord = words[n] + (firstWordInLine ? "" : " ");
const testLine = firstWordInLine ? testWord : line + testWord;
const metrics = context.measureText(testLine);
const testWidth = metrics.width;
if (testWidth > maxWidth && !firstWordInLine) {
context.fillText(line, x, currentY);
line = words[n] + " ";
currentY += lineHeight;
firstWordInLine = false;
} else {
line = firstWordInLine ? testWord : testLine;
firstWordInLine = false;
}
}
context.fillText(line, x, currentY);
return currentY + lineHeight;
}
return canvas;
}
Apply Changes