You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
nameText = "Your Name",
fontFamily = "Great Vibes, cursive", // Primary font (e.g., Google Font) first, then generic fallbacks
fontSize = 30, // In pixels
textColor = "#38220f", // Dark brown/sepia
backgroundColor = "#f0e6d6", // Antique white/cream
borderColor = "#5c3d21", // Medium brown/sepia for main border
borderWidth = 8, // Outer border thickness
cardWidth = 450,
cardHeight = 270,
imageOvalBorderColor = "#7a5c43", // Slightly lighter/different brown for image oval
imageOvalBorderWidth = 2,
cornerRadius = 15 // For the card's rounded corners
) {
const canvas = document.createElement('canvas');
canvas.width = cardWidth;
canvas.height = cardHeight;
const ctx = canvas.getContext('2d');
// Helper function for drawing rounded rectangles
function roundedRectPath(ctx, x, y, width, height, radius) {
if (width < 2 * radius) radius = width / 2;
if (height < 2 * radius) radius = height / 2;
if (radius < 0) radius = 0;
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.arcTo(x + width, y, x + width, y + height, radius);
ctx.arcTo(x + width, y + height, x, y + height, radius);
ctx.arcTo(x, y + height, x, y, radius);
ctx.arcTo(x, y, x + width, y, radius);
ctx.closePath();
}
// Attempt to load the primary font if it's "Great Vibes" (as an example of CDN font)
const primaryFontName = fontFamily.split(',')[0].trim().replace(/"/g, '');
if (primaryFontName === "Great Vibes") {
try {
const greatVibesFont = new FontFace('Great Vibes', 'url(https://fonts.gstatic.com/s/greatvibes/v14/RWmMoKWR9v4_unPq0pGRxcpoBGw.woff2)');
await greatVibesFont.load();
document.fonts.add(greatVibesFont);
} catch (e) {
console.warn("Could not load 'Great Vibes' font. Ensure network access or use a system font.", e);
}
}
// 1. Draw Background (with rounded corners)
roundedRectPath(ctx, 0, 0, canvas.width, canvas.height, cornerRadius);
ctx.fillStyle = backgroundColor;
ctx.fill();
// 2. Draw Borders
// Outer thick border
const outerBorderRadius = Math.max(0, cornerRadius - borderWidth / 2);
roundedRectPath(ctx, borderWidth / 2, borderWidth / 2, canvas.width - borderWidth, canvas.height - borderWidth, outerBorderRadius);
ctx.strokeStyle = borderColor;
ctx.lineWidth = borderWidth;
ctx.stroke();
// Inner thin decorative border
const innerBorderOffset = borderWidth + 3; // Gap from outer border's path start + thin border's own width adjustment
const innerBorderRadius = Math.max(0, cornerRadius - innerBorderOffset);
if (canvas.width - 2 * innerBorderOffset > 0 && canvas.height - 2 * innerBorderOffset > 0) {
roundedRectPath(ctx, innerBorderOffset, innerBorderOffset, canvas.width - 2 * innerBorderOffset, canvas.height - 2 * innerBorderOffset, innerBorderRadius);
ctx.strokeStyle = borderColor; // Could be a slightly different shade
ctx.lineWidth = 1;
ctx.stroke();
}
// Content padding: defines the main area for image and text
const contentPadding = innerBorderOffset + 5; // Extra space from the inner line
// 3. Image Placement (in an oval on the left)
const imgAreaLeft = contentPadding;
const imgVisibleAreaWidth = cardWidth * 0.32; // Proportion of card width for image oval
const imgOvalEffectiveWidth = imgVisibleAreaWidth - 2 * imageOvalBorderWidth;
const imgOvalEffectiveHeight = cardHeight * 0.70 - 2 * imageOvalBorderWidth; // Proportion of card height
const ovalCenterX = imgAreaLeft + imgVisibleAreaWidth / 2;
const ovalCenterY = cardHeight / 2;
const ovalRadiusX = imgOvalEffectiveWidth / 2;
const ovalRadiusY = imgOvalEffectiveHeight / 2;
if (ovalRadiusX > 0 && ovalRadiusY > 0) {
ctx.save();
ctx.beginPath();
ctx.ellipse(ovalCenterX, ovalCenterY, ovalRadiusX, ovalRadiusY, 0, 0, 2 * Math.PI);
// Stroke the
ctx.strokeStyle = imageOvalBorderColor;
ctx.lineWidth = imageOvalBorderWidth;
ctx.stroke();
ctx.clip(); // Clip to the oval path for drawing the image
// Calculate aspect ratios to draw the image to "cover" the oval
const imgAspect = originalImg.width / originalImg.height;
const ovalAspect = ovalRadiusX / ovalRadiusY; // ellipse radii aspect
let sx, sy, sWidth, sHeight;
const destWidth = ovalRadiusX * 2;
const destHeight = ovalRadiusY * 2;
if (imgAspect > ovalAspect) { // Image is wider than oval area, crop sides
sHeight = originalImg.height;
sWidth = sHeight * ovalAspect;
sx = (originalImg.width - sWidth) / 2;
sy = 0;
} else { // Image is taller or same aspect, crop top/bottom
sWidth = originalImg.width;
sHeight = sWidth / ovalAspect;
sx = 0;
sy = (originalImg.height - sHeight) / 2;
}
// Ensure sx, sy, sWidth, sHeight are valid
if (sWidth > 0 && sHeight > 0 && originalImg.width - sx >= sWidth && originalImg.height - sy >= sHeight) {
ctx.drawImage(originalImg, sx, sy, sWidth, sHeight,
ovalCenterX - ovalRadiusX, ovalCenterY - ovalRadiusY,
destWidth, destHeight);
} else {
// Fallback: draw entire image centered, scaled to fit (letterbox/pillarbox)
let fitScale = Math.min(destWidth / originalImg.width, destHeight / originalImg.height);
let fitWidth = originalImg.width * fitScale;
let fitHeight = originalImg.height * fitScale;
ctx.drawImage(originalImg,
ovalCenterX - fitWidth/2 , ovalCenterY - fitHeight/2,
fitWidth, fitHeight);
}
ctx.restore(); // Remove clipping path
}
// 4. Text Rendering (Name on the right)
const textStartX = imgAreaLeft + imgVisibleAreaWidth + cardWidth * 0.04; // Start X for text area with a gap
const textAreaRight = cardWidth - contentPadding; // End X for text area
const textAreaWidth = Math.max(0, textAreaRight - textStartX);
const textRenderX = textStartX + textAreaWidth / 2;
const textRenderY = cardHeight / 2;
ctx.fillStyle = textColor;
// Construct font string carefully for proper quoting and fallbacks
const fontParts = fontFamily.split(',').map(f => f.trim());
const mainFont = fontParts[0].includes(' ') ? `"${fontParts[0]}"` : fontParts[0];
const fallbacks = fontParts.slice(1).join(', ');
const fullFontString = `${fontSize}px ${mainFont}${fallbacks ? ', ' + fallbacks : ''}`;
ctx.font = fullFontString;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
if (textAreaWidth > 0) {
ctx.fillText(nameText, textRenderX, textRenderY, textAreaWidth); // Max width for fillText
}
// 5. Optional Flourishes around the text
if (textAreaWidth > 0) {
const textMetrics = ctx.measureText(nameText);
// Cap flourish length to not exceed text area or a proportion of text width
const flourishL = Math.min(textMetrics.width * 0.6, textAreaWidth * 0.8);
const flourishYOff = fontSize * 0.7; // Vertical offset from text baseline
const flourishCurveHeight = 5; // How much the flourish curves
if (flourishL > 10) { // Draw flourishes only if they are of a meaningful length
ctx.strokeStyle = textColor;
ctx.lineWidth = 1;
// Flourish above text
ctx.beginPath();
ctx.moveTo(textRenderX - flourishL / 2, textRenderY - flourishYOff);
ctx.quadraticCurveTo(textRenderX, textRenderY - flourishYOff - flourishCurveHeight, textRenderX + flourishL / 2, textRenderY - flourishYOff);
ctx.stroke();
// Flourish below text
ctx.beginPath();
ctx.moveTo(textRenderX - flourishL / 2, textRenderY + flourishYOff);
ctx.quadraticCurveTo(textRenderX, textRenderY + flourishYOff + flourishCurveHeight, textRenderX + flourishL / 2, textRenderY + flourishYOff);
ctx.stroke();
}
}
return canvas;
}
Apply Changes