You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
characterName = "PLAYER",
slotWidth = 120,
slotHeight = 150,
borderColor = "#444444",
selectedBorderColor = "gold",
borderThickness = 4,
selected = "false", // Should be string "true" or "false"
nameplateHeight = 25,
nameplateColor = "rgba(0,0,0,0.75)",
nameplateTextColor = "white",
fontFamily = "'Press Start 2P', monospace",
fontSize = 8,
imagePadding = 4,
textPadding = 4, // Horizontal padding for text in nameplate
cornerRadius = 6,
imageObjectFit = "cover", // "cover" or "contain"
imageBackgroundColor = "rgba(20,20,20,1)"
) {
const canvas = document.createElement('canvas');
canvas.width = slotWidth;
canvas.height = slotHeight;
const ctx = canvas.getContext('2d');
// 1. Font Loading (if 'Press Start 2P' is specified)
let effectiveFontFamily = fontFamily;
if (fontFamily.toLowerCase().includes("press start 2p")) {
const fontFaceName = 'Press Start 2P'; // Exact name for FontFace
let fontAvailable = false;
if (document.fonts) {
try {
fontAvailable = await document.fonts.check(`1px "${fontFaceName}"`);
} catch (e) {
// Some browsers might throw error on check (e.g. security restrictions)
// console.warn(`Font check for "${fontFaceName}" failed: ${e}. Will attempt to load.`);
}
}
if (!fontAvailable) {
const font = new FontFace(fontFaceName, 'url(https://fonts.gstatic.com/s/pressstart2p/v15/e3t4euO8T-267oIAQAu6PQfoundryitelg.woff2)');
try {
await font.load();
if (document.fonts) {
document.fonts.add(font);
} else {
// Fallback for very old browsers or environments without document.fonts
// console.warn("document.fonts API not available. Font may not render correctly.");
}
} catch (e) {
// console.warn(`Failed to load '${fontFaceName}' font. System fallbacks will be used. Error: ${e}`);
// Attempt to remove the failed font from the family string to allow CSS fallbacks
const regexSafeFaceName = fontFaceName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // Escape special chars
const fontPatterns = [
new RegExp(`^['"]?${regexSafeFaceName}['"]?,\\s*`, 'i'), // At the beginning, followed by a comma
new RegExp(`,\\s*['"]?${regexSafeFaceName}['"]?$`, 'i'), // At the end, preceded by a comma
new RegExp(`,\\s*['"]?${regexSafeFaceName}['"]?,\\s*`, 'i'), // In the middle
new RegExp(`^['"]?${regexSafeFaceName}['"]?$`, 'i') // Only font specified
];
for (const pattern of fontPatterns) {
if (pattern.test(effectiveFontFamily)) {
effectiveFontFamily = effectiveFontFamily.replace(pattern, pattern.source.includes(',') ? ',' : ''); // remove with comma or just font
break;
}
}
effectiveFontFamily = effectiveFontFamily.replace(/,\s*,/, ',').replace(/^,|,$/, '').trim();
if (!effectiveFontFamily) effectiveFontFamily = "monospace"; // Ensure there's always a fallback
}
}
}
// Helper for creating a rounded rectangle path
function pathRoundedRectHelper(x, y, w, h, rInput) {
let r = rInput;
if (typeof r === 'number') {
r = {tl: r, tr: r, br: r, bl: r};
} else { // Ensure all corners have a value
const defaultRadiusVal = 0;
r = {
tl: r.tl || defaultRadiusVal, tr: r.tr || defaultRadiusVal,
br: r.br || defaultRadiusVal, bl: r.bl || defaultRadiusVal
};
}
// Cap radius to prevent visual glitches
const minHalfDim = Math.min(w / 2, h / 2);
r.tl = Math.max(0, Math.min(r.tl, minHalfDim));
r.tr = Math.max(0, Math.min(r.tr, minHalfDim));
r.bl = Math.max(0, Math.min(r.bl, minHalfDim));
r.br = Math.max(0, Math.min(r.br, minHalfDim));
ctx.beginPath();
ctx.moveTo(x + r.tl, y);
ctx.lineTo(x + w - r.tr, y);
if (r.tr > 0) ctx.quadraticCurveTo(x + w, y, x + w, y + r.tr); else ctx.lineTo(x + w, y);
ctx.lineTo(x + w, y + h - r.br);
if (r.br > 0) ctx.quadraticCurveTo(x + w, y + h, x + w - r.br, y + h); else ctx.lineTo(x + w, y+h);
ctx.lineTo(x + r.bl, y + h);
if (r.bl > 0) ctx.quadraticCurveTo(x, y + h, x, y + h - r.bl); else ctx.lineTo(x, y+h);
ctx.lineTo(x, y + r.tl);
if (r.tl > 0) ctx.quadraticCurveTo(x, y, x + r.tl, y); else ctx.lineTo(x,y);
ctx.closePath();
}
const effectiveGlobalRadius = Math.max(0, cornerRadius);
// Calculate geometry for content area (inside the border)
const contentX = borderThickness;
const contentY = borderThickness;
const contentWidth = Math.max(0, slotWidth - 2 * borderThickness);
const contentHeight = Math.max(0, slotHeight - 2 * borderThickness);
const contentCornerRadius = Math.max(0, effectiveGlobalRadius - borderThickness);
// 2. Draw Slot Background (for the content area)
ctx.fillStyle = imageBackgroundColor;
if (contentWidth > 0 && contentHeight > 0) {
if (contentCornerRadius > 0) {
pathRoundedRectHelper(contentX, contentY, contentWidth, contentHeight, contentCornerRadius);
ctx.fill();
} else {
ctx.fillRect(contentX, contentY, contentWidth, contentHeight);
}
}
// 3. Draw Character Image
if (originalImg && originalImg.complete && originalImg.naturalWidth > 0 && originalImg.naturalHeight > 0) {
const imgBoxX = contentX + imagePadding;
const imgBoxY = contentY + imagePadding;
const imgBoxWidth = Math.max(0, contentWidth - 2 * imagePadding);
const imgBoxHeight = Math.max(0, contentHeight - nameplateHeight - 2 * imagePadding);
if (imgBoxWidth > 0 && imgBoxHeight > 0) {
ctx.save();
const imgClipRadius = Math.max(0, contentCornerRadius - imagePadding);
ctx.beginPath();
ctx.moveTo(imgBoxX + imgClipRadius, imgBoxY);
ctx.lineTo(imgBoxX + imgBoxWidth - imgClipRadius, imgBoxY);
if (imgClipRadius > 0) ctx.quadraticCurveTo(imgBoxX + imgBoxWidth, imgBoxY, imgBoxX + imgBoxWidth, imgBoxY + imgClipRadius); else ctx.lineTo(imgBoxX + imgBoxWidth, imgBoxY);
ctx.lineTo(imgBoxX + imgBoxWidth, imgBoxY + imgBoxHeight);
ctx.lineTo(imgBoxX, imgBoxY + imgBoxHeight);
ctx.lineTo(imgBoxX, imgBoxY + imgClipRadius);
if (imgClipRadius > 0) ctx.quadraticCurveTo(imgBoxX, imgBoxY, imgBoxX + imgClipRadius, imgBoxY); else ctx.lineTo(imgBoxX, imgBoxY);
ctx.closePath();
ctx.clip();
const imgNaturalAspect = originalImg.naturalWidth / originalImg.naturalHeight;
const imgBoxAspect = imgBoxWidth / imgBoxHeight;
let dWidth, dHeight, dXDraw, dYDraw;
if (imageObjectFit === "cover") {
if (imgNaturalAspect > imgBoxAspect) {
dHeight = imgBoxHeight;
dWidth = dHeight * imgNaturalAspect;
} else {
dWidth = imgBoxWidth;
dHeight = dWidth / imgNaturalAspect;
}
} else { // "contain"
if (imgNaturalAspect > imgBoxAspect) {
dWidth = imgBoxWidth;
dHeight = dWidth / imgNaturalAspect;
} else {
dHeight = imgBoxHeight;
dWidth = dHeight * imgNaturalAspect;
}
}
dXDraw = imgBoxX + (imgBoxWidth - dWidth) / 2;
dYDraw = imgBoxY + (imgBoxHeight - dHeight) / 2;
ctx.drawImage(originalImg, 0, 0, originalImg.naturalWidth, originalImg.naturalHeight, dXDraw, dYDraw, dWidth, dHeight);
ctx.restore();
}
}
// 4. Draw Nameplate Background
const nameplateDrawX = contentX;
const nameplateDrawY = Math.max(contentY, contentY + contentHeight - nameplateHeight);
const nameplateDrawWidth = contentWidth;
const actualNameplateHeight = Math.min(nameplateHeight, contentHeight);
if (nameplateDrawWidth > 0 && actualNameplateHeight > 0) {
ctx.fillStyle = nameplateColor;
ctx.beginPath();
ctx.moveTo(nameplateDrawX, nameplateDrawY);
ctx.lineTo(nameplateDrawX + nameplateDrawWidth, nameplateDrawY);
ctx.lineTo(nameplateDrawX + nameplateDrawWidth, nameplateDrawY + actualNameplateHeight - contentCornerRadius);
if (contentCornerRadius > 0) ctx.quadraticCurveTo(nameplateDrawX + nameplateDrawWidth, nameplateDrawY + actualNameplateHeight, nameplateDrawX + nameplateDrawWidth - contentCornerRadius, nameplateDrawY + actualNameplateHeight); else ctx.lineTo(nameplateDrawX + nameplateDrawWidth, nameplateDrawY + actualNameplateHeight);
ctx.lineTo(nameplateDrawX + contentCornerRadius, nameplateDrawY + actualNameplateHeight);
if (contentCornerRadius > 0) ctx.quadraticCurveTo(nameplateDrawX, nameplateDrawY + actualNameplateHeight, nameplateDrawX, nameplateDrawY + actualNameplateHeight - contentCornerRadius); else ctx.lineTo(nameplateDrawX, nameplateDrawY + actualNameplateHeight);
ctx.closePath();
ctx.fill();
}
// 5. Draw Nameplate Text
if (nameplateDrawWidth > 0 && actualNameplateHeight > 0 && fontSize > 0) {
ctx.fillStyle = nameplateTextColor;
ctx.font = `${fontSize}px ${effectiveFontFamily}`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const textMaxWidth = Math.max(0, nameplateDrawWidth - 2 * textPadding);
const textX = nameplateDrawX + nameplateDrawWidth / 2;
const textY = nameplateDrawY + actualNameplateHeight / 2; // Center vertically in nameplate
if (textMaxWidth > 0) {
ctx.fillText(characterName, textX, textY, textMaxWidth);
}
}
// 6. Draw Border
if (borderThickness > 0) {
ctx.strokeStyle = (String(selected).toLowerCase() === "true") ? selectedBorderColor : borderColor;
ctx.lineWidth = borderThickness;
const borderPathX = borderThickness / 2;
const borderPathY = borderThickness / 2;
const borderPathWidth = Math.max(0, slotWidth - borderThickness);
const borderPathHeight = Math.max(0, slotHeight - borderThickness);
const borderPathRadius = Math.max(0, effectiveGlobalRadius - borderThickness / 2);
if (borderPathWidth > 0 && borderPathHeight > 0) {
if (borderPathRadius > 0) {
pathRoundedRectHelper(borderPathX, borderPathY, borderPathWidth, borderPathHeight, borderPathRadius);
ctx.stroke();
} else {
ctx.strokeRect(borderPathX, borderPathY, borderPathWidth, borderPathHeight);
}
}
}
return canvas;
}
Apply Changes