You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg,
recipientName = "Jane Doe",
awardReason = "Excellence in JavaScript Development",
dateText = "", // Default to current date if empty string
issuedBy = "The Code Masters",
certificateTitle = "Certificate of Achievement",
presenterLine = "This certificate is proudly presented to",
mainColor = "#003366", // Dark Blue
accentColor = "#B8860B", // DarkGoldenrod
fontFamilyTitleParam = "Times New Roman, serif",
fontFamilyBodyParam = "Georgia, serif",
fontFamilyScriptParam = "Great Vibes, cursive", // Example: 'Great Vibes, cursive' or 'Zapfino, cursive'
signaturePlaceholder = "Authorized Signature",
issuedByTitle = "Issuing Authority",
logoPlacement = "top-center", // "top-left", "top-right", "seal", "none"
logoSize = 100, // Max dimension (width or height) for the logo image, forming a bounding box.
backgroundColor = "#FDF5E6" // Old Lace / Antique White
) {
// Helper function to load fonts dynamically (e.g., from Google Fonts)
async function loadFont(fontName, fontUrl) {
const fontId = `dynamic-font-${fontName.replace(/\s+/g, '-').toLowerCase()}`;
if (!document.getElementById(fontId)) {
const link = document.createElement('link');
link.id = fontId;
link.rel = 'stylesheet';
link.href = fontUrl;
document.head.appendChild(link);
try {
await document.fonts.load(`1em "${fontName}"`);
} catch (e) {
console.warn(`Failed to load font: ${fontName} from ${fontUrl}. Error: ${e}`);
}
}
}
// Helper function to wrap text
function wrapText(context, text, x, y, maxWidth, lineHeight) {
const words = text.split(' ');
let lineBuffer = '';
let currentDrawingY = y; // Y for drawing current line (baseline)
const initialBaseline = context.textBaseline;
// Ensure consistent baseline for calculations & drawing, alphabetic is common.
context.textBaseline = 'alphabetic';
for (let n = 0; n < words.length; n++) {
const testLine = lineBuffer + words[n] + ' ';
const metrics = context.measureText(testLine);
const testWidth = metrics.width;
if (testWidth > maxWidth && n > 0) {
context.fillText(lineBuffer.trim(), x, currentDrawingY);
lineBuffer = words[n] + ' ';
currentDrawingY += lineHeight;
} else {
lineBuffer = testLine;
}
}
context.fillText(lineBuffer.trim(), x, currentDrawingY); // Draw the last line
context.textBaseline = initialBaseline; // Restore original baseline
return currentDrawingY; // Returns the Y baseline of the last drawn line
}
// Load script font if specified (example 'Great Vibes')
const primaryScriptFont = fontFamilyScriptParam.split(',')[0].trim().toLowerCase();
if (primaryScriptFont === 'great vibes') {
await loadFont('Great Vibes', 'https://fonts.googleapis.com/css2?family=Great+Vibes&display=swap');
}
// Other fonts could be loaded here if specified as known web fonts in parameters.
// Canvas setup
const canvas = document.createElement('canvas');
const RENDER_WIDTH = 1000;
const RENDER_HEIGHT = 700;
canvas.width = RENDER_WIDTH;
canvas.height = RENDER_HEIGHT;
const ctx = canvas.getContext('2d');
// Date default
if (!dateText.trim()) {
dateText = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
}
// Fill background
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, RENDER_WIDTH, RENDER_HEIGHT);
// Border
const borderWidthOuter = 5;
const borderWidthInner = 2;
const padding = 20; // Outer padding for the first border
ctx.strokeStyle = mainColor;
ctx.lineWidth = borderWidthOuter;
ctx.strokeRect(padding, padding, RENDER_WIDTH - 2 * padding, RENDER_HEIGHT - 2 * padding);
const innerBorderPadding = padding + borderWidthOuter + 5;
ctx.strokeStyle = accentColor;
ctx.lineWidth = borderWidthInner;
ctx.strokeRect(innerBorderPadding, innerBorderPadding,
RENDER_WIDTH - 2 * innerBorderPadding,
RENDER_HEIGHT - 2 * innerBorderPadding);
// Logo (if not used as seal)
let logoTopOffset = innerBorderPadding + 40; // Default top margin
if (originalImg && originalImg.complete && originalImg.naturalWidth > 0 &&
logoPlacement !== 'none' && logoPlacement !== 'seal' && logoSize > 0) {
let w = originalImg.naturalWidth;
let h = originalImg.naturalHeight;
let logoDisplayWidth, logoDisplayHeight;
const aspectRatio = w / h;
if (aspectRatio >= 1) { // Wider than tall, or square
logoDisplayWidth = logoSize;
logoDisplayHeight = logoSize / aspectRatio;
} else { // Taller than wide
logoDisplayHeight = logoSize;
logoDisplayWidth = logoSize * aspectRatio;
}
let logoX = -1, logoY = -1;
const logoMargin = 20; // Margin from inner border edge
switch (logoPlacement) {
case 'top-center':
logoX = RENDER_WIDTH / 2 - logoDisplayWidth / 2;
logoY = innerBorderPadding + logoMargin;
break;
case 'top-left':
logoX = innerBorderPadding + logoMargin;
logoY = innerBorderPadding + logoMargin;
break;
case 'top-right':
logoX = RENDER_WIDTH - innerBorderPadding - logoMargin - logoDisplayWidth;
logoY = innerBorderPadding + logoMargin;
break;
}
if (logoX !== -1 ) {
if (logoPlacement.startsWith('top-')) {
logoTopOffset = logoY + logoDisplayHeight + 20;
}
ctx.drawImage(originalImg, logoX, logoY, logoDisplayWidth, logoDisplayHeight);
}
}
// Content
ctx.fillStyle = mainColor;
ctx.textAlign = 'center';
ctx.textBaseline = 'alphabetic';
let currentY = logoTopOffset;
// Certificate Title
ctx.font = `bold 48px ${fontFamilyTitleParam}`;
ctx.fillText(certificateTitle, RENDER_WIDTH / 2, currentY);
currentY += 48 * 0.7; // Approx height used by title text
// Decorative line under title
currentY += 20; // Space above line
ctx.beginPath();
ctx.moveTo(RENDER_WIDTH / 2 - 150, currentY);
ctx.lineTo(RENDER_WIDTH / 2 + 150, currentY);
ctx.strokeStyle = accentColor;
ctx.lineWidth = 1.5;
ctx.stroke();
currentY += 35; // Space after line
// Presenter Line
ctx.font = `24px ${fontFamilyBodyParam}`;
ctx.fillStyle = mainColor;
ctx.fillText(presenterLine, RENDER_WIDTH / 2, currentY);
currentY += 24 + 20; // Text height + space
// Recipient Name - script fonts often need y adjustment
ctx.font = `bold 60px ${fontFamilyScriptParam}`;
ctx.fillStyle = accentColor;
// Use 'middle' baseline for more predictable vertical centering of script fonts
ctx.textBaseline = 'middle';
const recipientNameY = currentY + (60 * 0.5); // Center the text block roughly
ctx.fillText(recipientName, RENDER_WIDTH / 2, recipientNameY);
ctx.textBaseline = 'alphabetic'; // Reset
currentY = recipientNameY + (60 * 0.5) + 20; // Approx bottom of text + space
// Award Reason
ctx.fillStyle = mainColor;
ctx.font = `22px ${fontFamilyBodyParam}`;
let awardReasonLineHeight = 28;
// The y for wrapText is the baseline of the first line.
let firstLineReasonY = currentY + awardReasonLineHeight * 0.8; // Start first line's baseline
let lastLineReasonY = wrapText(ctx, awardReason, RENDER_WIDTH / 2, firstLineReasonY, RENDER_WIDTH - 2 * (innerBorderPadding + 30), awardReasonLineHeight);
currentY = lastLineReasonY + 20; // Space after reason text
// Bottom section: Date and Signature
const bottomSectionLineY = RENDER_HEIGHT - innerBorderPadding - 70; // Y coord of the signature/date lines
// Date
const dateAreaX = innerBorderPadding + 150;
ctx.font = `16px ${fontFamilyBodyParam}`;
ctx.fillStyle = mainColor;
ctx.textAlign = 'center';
ctx.fillText("Date: " + dateText, dateAreaX, bottomSectionLineY + 25); // Text below line
ctx.beginPath(); // Line for Date
ctx.moveTo(dateAreaX - 100, bottomSectionLineY);
ctx.lineTo(dateAreaX + 100, bottomSectionLineY);
ctx.strokeStyle = mainColor;
ctx.lineWidth = 1;
ctx.stroke();
// Signature & Issued By
const signatureAreaX = RENDER_WIDTH - innerBorderPadding - 150;
ctx.textAlign = 'center';
ctx.font = `20px ${fontFamilyScriptParam}`;
ctx.fillStyle = mainColor;
ctx.textBaseline = 'middle';
ctx.fillText(signaturePlaceholder, signatureAreaX, bottomSectionLineY - (20*0.3) ); // Position name slightly above the line
ctx.textBaseline = 'alphabetic';
ctx.beginPath(); // Line for Signature
ctx.moveTo(signatureAreaX - 125, bottomSectionLineY);
ctx.lineTo(signatureAreaX + 125, bottomSectionLineY);
ctx.strokeStyle = mainColor;
ctx.lineWidth = 1;
ctx.stroke();
ctx.font = `16px ${fontFamilyBodyParam}`;
ctx.fillStyle = mainColor;
ctx.fillText(issuedBy, signatureAreaX, bottomSectionLineY + 25);
if (issuedByTitle) {
ctx.font = `italic 14px ${fontFamilyBodyParam}`;
ctx.fillText(issuedByTitle, signatureAreaX, bottomSectionLineY + 25 + 18);
}
// Seal
const sealRadius = 35;
const sealX = RENDER_WIDTH / 2;
// Calculate vertical space for seal: between award reason and signature lines
const spaceForSealTop = currentY + sealRadius + 10; // Bottom of awardReason text + margin
const spaceForSealBottom = bottomSectionLineY - (16 * 1.5) - sealRadius - 10; // Top of signature lines area - margin
let sealFinalY = spaceForSealTop + (spaceForSealBottom - spaceForSealTop) / 2;
const canFitSeal = (spaceForSealBottom >= spaceForSealTop) && (logoPlacement === 'seal' || logoSize>0) ; // also check logoSize for seal context
if (canFitSeal) {
sealFinalY = Math.max(sealFinalY, spaceForSealTop); // ensure it's below award reason
sealFinalY = Math.min(sealFinalY, spaceForSealBottom); // ensure it's above signatures
if (logoPlacement === 'seal' && originalImg && originalImg.complete && originalImg.naturalWidth > 0) {
ctx.save();
ctx.beginPath();
ctx.arc(sealX, sealFinalY, sealRadius, 0, Math.PI * 2, true);
ctx.clip();
ctx.drawImage(originalImg, sealX - sealRadius, sealFinalY - sealRadius, sealRadius * 2, sealRadius * 2);
ctx.restore();
ctx.beginPath(); // Border for image seal
ctx.arc(sealX, sealFinalY, sealRadius, 0, 2 * Math.PI, false);
ctx.strokeStyle = mainColor;
ctx.lineWidth = 1.5;
ctx.stroke();
} else if (logoPlacement !== 'seal') { // Draw default graphic seal only if originalImg is not meant for seal
ctx.beginPath();
ctx.arc(sealX, sealFinalY, sealRadius, 0, 2 * Math.PI, false);
ctx.fillStyle = accentColor;
ctx.fill();
ctx.lineWidth = 2;
ctx.strokeStyle = mainColor;
ctx.stroke();
ctx.font = `bold ${sealRadius * 0.4}px ${fontFamilyTitleParam}`;
// Simple color contrast for seal text: use background color, assumes it's light.
ctx.fillStyle = backgroundColor;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText("SEAL", sealX, sealFinalY);
}
}
ctx.textBaseline = 'alphabetic'; // Reset to default for safety
ctx.textAlign = 'left';
return canvas;
}
Apply Changes