You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, titleParam, mainEventParam, dateParam, timeParam, locationParam, hostParam, rsvpInfoParam, additionalTextParam) {
// Helper function to load fonts
async function loadFont(fontName, fontUrl, descriptors) {
const font = new FontFace(fontName, `url(${fontUrl})`, descriptors || {});
try {
await font.load();
document.fonts.add(font);
} catch (e) {
console.error(`Font ${fontName} could not be loaded: ${e}. Using system default.`);
// Fallback or warning could be implemented here if critical
}
}
// Helper function to wrap and draw text
// y: baseline of the first line of text to be drawn.
// Returns: the Y coordinate for the baseline of the line *after* the last line of text drawn.
// If input text is empty/whitespace, returns the original y (baseline).
function wrapTextAndDraw(context, text, x, y, maxWidth, lineHeight, fontStyle) {
const sText = String(text).trim();
if (!sText) {
return y; // Return original baseline if text is empty. Spacing to next block is handled by caller.
}
if (fontStyle) context.font = fontStyle; // Set font for measurement and drawing
const words = sText.split(' ');
let line = '';
let currentLineBaseY = y; // Baseline for the current line being assembled/drawn
for (let n = 0; n < words.length; n++) {
const testLine = line + words[n] + ' ';
const metrics = context.measureText(testLine); // Requires font to be set on context
const testWidth = metrics.width;
if (testWidth > maxWidth && n > 0) { // If line is too wide and it's not the first word
context.fillText(line.trim(), x, currentLineBaseY); // Draw the previous line
line = words[n] + ' '; // Start a new line
currentLineBaseY += lineHeight; // Move baseline down for the new line
} else {
line = testLine; // Add word to current line
}
}
context.fillText(line.trim(), x, currentLineBaseY); // Draw the last line
return currentLineBaseY + lineHeight; // Return baseline for the line that would follow this block
}
// --- Parameter Defaults ---
const title = titleParam === undefined ? "A Solemn Summons" : String(titleParam);
const mainEvent = mainEventParam === undefined ? "To Commune with the Spirits" : String(mainEventParam);
const date = dateParam === undefined ? "The Eve of All Hallows, Anno Domini 1888" : String(dateParam);
const time = timeParam === undefined ? "At the Stroke of Midnight" : String(timeParam);
const location = locationParam === undefined ? "The Blackwood Manor, Grimstone Lane" : String(locationParam);
const host = hostParam === undefined ? "Madame Seraphina" : String(hostParam);
const rsvpInfo = rsvpInfoParam === undefined ? "Kindly respond via Ouija board, posthaste." : String(rsvpInfoParam);
const additionalText = additionalTextParam === undefined ? "The veil is thin, the spirits await your presence..." : String(additionalTextParam);
// --- Font Loading ---
await Promise.all([
loadFont('IM Fell English', 'https://fonts.gstatic.com/s/imfellenglish/v12/Ktk1ALSLW8zRujuFEjOcVdusaIH0EGB4Dkbx-hU.woff2'),
loadFont('IM Fell DW Pica', 'https://fonts.gstatic.com/s/imfelldwpica/v12/2sDGZGRQotv9nbn2q3qc0g5sXLFG02wdwaA-.woff2'),
loadFont('Mrs Saint Delafield', 'https://fonts.gstatic.com/s/mrssaintdelafield/v12/v6-IGZd7ZwOc1sNfnZUE3D4VgJ3J79fLhmPmmatq.woff2')
]);
// --- Canvas Setup ---
const canvas = document.createElement('canvas');
canvas.width = 600;
canvas.height = 900; // Vertical format for invitation
const ctx = canvas.getContext('2d');
const backgroundColor = '#3a312a'; // Aged dark brown paper
const textColor = '#d8c8b0'; // Aged parchment/cream text
const borderColor = '#241a12'; // Very dark brown for border accents
const padding = 40; // General padding from canvas edges
const centerX = canvas.width / 2;
const contentWidth = canvas.width - 2 * padding;
// --- Background Drawing ---
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Subtle Vignette
const vignetteGrad = ctx.createRadialGradient(
centerX, canvas.height / 2, Math.min(canvas.width, canvas.height) * 0.25, // Inner circle
centerX, canvas.height / 2, Math.max(canvas.width, canvas.height) * 0.75 // Outer circle
);
vignetteGrad.addColorStop(0, 'rgba(0,0,0,0)'); // Center is transparent
vignetteGrad.addColorStop(1, 'rgba(0,0,0,0.4)'); // Edges are subtly darker
ctx.fillStyle = vignetteGrad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Decorative Border
ctx.strokeStyle = borderColor;
ctx.lineWidth = 1;
ctx.strokeRect(padding / 2, padding / 2, canvas.width - padding, canvas.height - padding);
ctx.lineWidth = 3;
ctx.strokeRect(padding / 2 + 4, padding / 2 + 4, canvas.width - padding - 8, canvas.height - padding - 8);
// --- Text and Image Layout ---
ctx.textAlign = 'center';
ctx.fillStyle = textColor;
let currentY = padding + 10; // Initial Y position for the top of the first text block's content. This will be adjusted to become baseline.
// Title
const titleFontSize = 44;
const titleLineHeight = titleFontSize + 6;
currentY += titleFontSize; // Adjust currentY to be the baseline of the first line of the title
currentY = wrapTextAndDraw(ctx, title, centerX, currentY, contentWidth, titleLineHeight, `bold ${titleFontSize}px "IM Fell English"`);
currentY += 25; // Spacing after title block (gap to the next element's baseline)
// Main Event
const mainEventFontSize = 30;
const mainEventLineHeight = mainEventFontSize + 6;
currentY += mainEventFontSize; // Adjust for baseline
currentY = wrapTextAndDraw(ctx, mainEvent, centerX, currentY, contentWidth * 0.9, mainEventLineHeight, `${mainEventFontSize}px "IM Fell DW Pica"`);
currentY += 35;
// Image Section
const imageSectionTopY = currentY - mainEventFontSize; // Top Y where the image section visually starts
const imageSectionHeight = 180; // Allocated vertical space for the image and its frame
const imageFramePadding = 5; // Padding around the image for its frame strokes
let sImgW = originalImg.width;
let sImgH = originalImg.height;
const imgMaxDisplayWidth = contentWidth * 0.6; // Image won't take full content width
const imgMaxDisplayHeight = imageSectionHeight - 2 * imageFramePadding;
const aspectRatio = Math.min(imgMaxDisplayWidth / sImgW, imgMaxDisplayHeight / sImgH);
sImgW *= aspectRatio;
sImgH *= aspectRatio;
const actualImgX = centerX - sImgW / 2;
const actualImgY = imageSectionTopY + (imageSectionHeight - sImgH) / 2; // Vertically center image in its slot
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = originalImg.width;
tempCanvas.height = originalImg.height;
tempCtx.filter = 'sepia(0.7) grayscale(0.15) contrast(0.9) brightness(0.95)';
tempCtx.drawImage(originalImg, 0, 0);
ctx.drawImage(tempCanvas, actualImgX, actualImgY, sImgW, sImgH);
// Image Frame
ctx.strokeStyle = borderColor;
ctx.lineWidth = 1;
ctx.strokeRect(actualImgX - imageFramePadding + 2, actualImgY - imageFramePadding + 2, sImgW + 2 * imageFramePadding - 4, sImgH + 2 * imageFramePadding - 4);
ctx.strokeStyle = 'rgba(216,200,176,0.35)'; // Lighter highlight for frame
ctx.lineWidth = 2;
ctx.strokeRect(actualImgX - imageFramePadding, actualImgY - imageFramePadding, sImgW + 2 * imageFramePadding, sImgH + 2 * imageFramePadding);
currentY = imageSectionTopY + imageSectionHeight + mainEventFontSize; // Update currentY to be baseline after image section. Tricky due to baseline mgmt.
// Simpler: currentY = (baseline of prev block) + ( allocated image section height )
// currentY was baseline AFTER mainEvent. Add imageSectionHeight to it + spacing.
currentY += imageSectionHeight - mainEventFontSize; // Rough adjustment
currentY += 25; // Spacing after image section
// Hosted by
const hostFontSize = 24; // Using script font, can be larger
const hostLineHeight = hostFontSize + 4;
const hostText = host ? `Hosted by ${host}` : "";
if (hostText) {
currentY += hostFontSize;
currentY = wrapTextAndDraw(ctx, hostText, centerX, currentY, contentWidth * 0.85, hostLineHeight, `${hostFontSize}px "Mrs Saint Delafield"`);
currentY += 20;
}
// Details Section: When & Where
const detailLabelFontSize = 20;
const detailTextFontSize = 18;
const detailLineHeight = detailTextFontSize + 4;
let detailLabelUsed = false;
if (date || time || location) {
currentY += detailLabelFontSize;
ctx.font = `bold ${detailLabelFontSize}px "IM Fell DW Pica"`;
ctx.fillText("Particulars:", centerX, currentY); // One label for all details
currentY += detailLabelFontSize * 0.6; // Spacing after label
detailLabelUsed = true;
}
if (date) {
currentY += detailTextFontSize;
currentY = wrapTextAndDraw(ctx, date, centerX, currentY, contentWidth * 0.8, detailLineHeight, `${detailTextFontSize}px "IM Fell English"`);
if (!time && !location) currentY += (detailLabelUsed ? 20 : 0); // Add final spacing if this is last detail
}
if (time) {
currentY += detailTextFontSize * (date ? 0.2 : 1); // Smaller gap if date preceded, full if time is first
currentY = wrapTextAndDraw(ctx, time, centerX, currentY, contentWidth * 0.8, detailLineHeight, `${detailTextFontSize}px "IM Fell English"`);
if (!location) currentY += (detailLabelUsed ? 20 : 0);
}
if (location) {
currentY += detailTextFontSize * ((date || time) ? 0.2 : 1);
currentY = wrapTextAndDraw(ctx, location, centerX, currentY, contentWidth * 0.8, detailLineHeight, `${detailTextFontSize}px "IM Fell English"`);
currentY += (detailLabelUsed ? 20 : 0);
}
if ( !detailLabelUsed && (date || time || location)) currentY += 10; // Small spacing if details existed without label
// Additional Text
const additionalFontSize = 18;
const additionalLineHeight = additionalFontSize + 4;
if (additionalText) {
currentY += additionalFontSize;
currentY = wrapTextAndDraw(ctx, additionalText, centerX, currentY, contentWidth * 0.85, additionalLineHeight, `italic ${additionalFontSize}px "IM Fell English"`);
currentY += 25;
}
// RSVP Info - position near bottom, but allow slight flow
const rsvpFontSize = 16;
const rsvpLineHeight = rsvpFontSize + 4;
const rsvpDesiredY = canvas.height - padding - rsvpLineHeight * 2; // Ideal Y for RSVP block start
if (rsvpInfo) {
// If currentY is already past desired, use currentY. Otherwise, jump to desiredY if it's further down.
currentY = Math.max(currentY, rsvpDesiredY);
currentY += rsvpFontSize;
// Ensure it does not draw off-canvas
if (currentY + rsvpLineHeight < canvas.height - padding/2) {
wrapTextAndDraw(ctx, rsvpInfo, centerX, currentY, contentWidth, rsvpLineHeight, `${rsvpFontSize}px "IM Fell DW Pica"`);
} else { // Fallback if too low (should rarely happen with above logic)
wrapTextAndDraw(ctx, rsvpInfo, centerX, canvas.height - padding/2 - rsvpFontSize, contentWidth, rsvpLineHeight, `${rsvpFontSize}px "IM Fell DW Pica"`);
}
}
return canvas;
}
Apply Changes