You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
travelerName = "Jane Quantum",
originDimension = "Prime Reality 7",
destinationDimension = "All Valid Timelines (Class B)",
permitID = "IDP-" + Math.random().toString(36).substring(2, 9).toUpperCase(),
issueDate = new Date().toISOString().slice(0, 10),
expiryDate = "PERPETUAL (Subject to Review)",
authorization = "Interdimensional High Council",
permitTitle = "INTERDIMENSIONAL TRAVEL PERMIT",
permitSubTitle = "(Under Universal Compact Art. IV, Sec. Beta-7)",
permitColor = "#F0EAD6", // Parchment/Off-white
textColor = "#2C2C2C", // Very dark gray
borderColor = "#4A3B31", // Dark brown
accentColor = "#8C1C1C", // Dark red for seal and thin border
fontFamily = "'Times New Roman', Times, serif",
officialSealText = "IHC",
photoBorderColor = "#333333" // Dark gray for photo frame
) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const permitWidth = 750;
const permitHeight = 500;
canvas.width = permitWidth;
canvas.height = permitHeight;
const padding = 25; // General padding from canvas edges
// 1. Background
ctx.fillStyle = permitColor;
ctx.fillRect(0, 0, permitWidth, permitHeight);
// 2. Main Border
const mainBorderWidth = 10;
ctx.strokeStyle = borderColor;
ctx.lineWidth = mainBorderWidth;
const BORDER_OFFSET = mainBorderWidth / 2;
ctx.strokeRect(BORDER_OFFSET, BORDER_OFFSET, permitWidth - mainBorderWidth, permitHeight - mainBorderWidth);
// Optional: A thin inner line for decoration
ctx.strokeStyle = accentColor;
ctx.lineWidth = 1;
const thinBorderPadding = BORDER_OFFSET + mainBorderWidth/2 + 5; // Padding from canvas edge for this thin line
ctx.strokeRect(thinBorderPadding, thinBorderPadding, permitWidth - thinBorderPadding * 2, permitHeight - thinBorderPadding * 2);
// 3. Title and Subtitle
ctx.fillStyle = textColor;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle'; // Align text vertically to its center for easier Y positioning
const titleFontSize = 30;
ctx.font = `bold ${titleFontSize}px ${fontFamily}`;
// Place title considering padding and its own height
const titleY = padding + titleFontSize / 2 + 5;
ctx.fillText(permitTitle, permitWidth / 2, titleY);
let lastTextBottomY = titleY + titleFontSize / 2;
if (permitSubTitle && permitSubTitle.trim() !== "") {
const subTitleFontSize = 13;
ctx.font = `${subTitleFontSize}px ${fontFamily}`;
// Position subtitle below title
const subTitleY = lastTextBottomY + subTitleFontSize / 2 + 8;
ctx.fillText(permitSubTitle, permitWidth / 2, subTitleY);
lastTextBottomY = subTitleY + subTitleFontSize / 2;
}
ctx.textBaseline = 'alphabetic'; // Reset baseline to default
// 4. Profile Picture
const photoBoxX = padding + 20;
// Y position below title/subtitle block
const photoBoxY = lastTextBottomY + 20;
const photoBoxWidth = 170;
const photoBoxHeight = 210;
// Draw photo border
ctx.strokeStyle = photoBorderColor;
ctx.lineWidth = 2;
ctx.strokeRect(photoBoxX -1, photoBoxY -1, photoBoxWidth + 2, photoBoxHeight + 2);
// Fit image into photoBoxWidth and photoBoxHeight, maintaining aspect ratio
if (originalImg && originalImg.width > 0 && originalImg.height > 0) {
let drawWidth = originalImg.width;
let drawHeight = originalImg.height;
const imgAspect = originalImg.width / originalImg.height;
const boxAspect = photoBoxWidth / photoBoxHeight;
if (imgAspect > boxAspect) { // Image is wider than box definition
drawWidth = photoBoxWidth;
drawHeight = photoBoxWidth / imgAspect;
} else { // Image is taller or same aspect as box definition
drawHeight = photoBoxHeight;
drawWidth = photoBoxHeight * imgAspect;
}
const offsetX = (photoBoxWidth - drawWidth) / 2;
const offsetY = (photoBoxHeight - drawHeight) / 2;
ctx.drawImage(originalImg, photoBoxX + offsetX, photoBoxY + offsetY, drawWidth, drawHeight);
} else { // Fallback if image is invalid or not loaded
ctx.fillStyle = '#E0E0E0'; // Light gray placeholder
ctx.fillRect(photoBoxX, photoBoxY, photoBoxWidth, photoBoxHeight);
ctx.fillStyle = textColor;
ctx.font = `14px ${fontFamily}`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText("Image Error", photoBoxX + photoBoxWidth/2, photoBoxY + photoBoxHeight/2);
ctx.textAlign = 'left'; // Reset alignment
ctx.textBaseline = 'alphabetic'; // Reset baseline
}
// 5. Text Information Section (to the right of the photo)
ctx.fillStyle = textColor;
ctx.textAlign = 'left';
let currentTextY = photoBoxY + 5; // Align first text line near top of photo box
const textFieldsX = photoBoxX + photoBoxWidth + 25;
const infoLineHeight = 26;
const valueColumnXOffset = 145; // Horizontal offset for the value part from textFieldsX
const printDetail = (label, value) => {
// Stop if drawing too low (e.g., overlapping seal/signature area)
if (currentTextY > permitHeight - padding - 70) return;
ctx.textAlign = 'left';
// Draw Label
ctx.font = `16px ${fontFamily}`;
const labelText = `${label}:`;
ctx.fillText(labelText, textFieldsX, currentTextY);
// Draw Value (with basic word wrapping)
ctx.font = `bold 16px ${fontFamily}`;
const valueDrawX = textFieldsX + valueColumnXOffset;
const maxValueWidth = permitWidth - valueDrawX - padding - 10; // Max width for value text before wrapping
let words = String(value).split(' '); // Ensure value is string
let currentLine = '';
let lineY = currentTextY; // Y for the current line of the value
for (let i = 0; i < words.length; i++) {
let testLine = currentLine + words[i] + ' ';
if (ctx.measureText(testLine).width > maxValueWidth && i > 0) {
ctx.fillText(currentLine.trim(), valueDrawX, lineY); // Draw the completed line
currentLine = words[i] + ' '; // Start new line
lineY += 18; // Move Y down for the new wrapped line (18px for 16px bold font)
} else {
currentLine = testLine;
}
}
ctx.fillText(currentLine.trim(), valueDrawX, lineY); // Draw the last or only line
currentTextY = lineY + infoLineHeight; // Advance Y for the next detail field
};
printDetail("Traveler Name", travelerName);
printDetail("Permit ID", permitID);
printDetail("Origin Dimension", originDimension);
printDetail("Destination(s)", destinationDimension);
printDetail("Date of Issue", issueDate);
printDetail("Validity Period", expiryDate);
printDetail("Authorized By", authorization);
// 6. Disclaimer Text (below main details, if space allows)
let disclaimerCurrentY = Math.max(currentTextY, photoBoxY + photoBoxHeight + 20); // Start disclaimer below photo and details
ctx.font = `italic 11px ${fontFamily}`;
const disclaimerText = "Holder must adhere to all temporal, dimensional, and existential bylaws. Non-compliance may result in paradox containment, timeline pruning, or summary de-resolution by authorized entities.";
const disclaimerLines = [];
const maxDisclaimerWidth = permitWidth - textFieldsX - padding - 10;
if (disclaimerCurrentY < permitHeight - padding - 60) { // Check if there's enough vertical space for at least some disclaimer
let currentLine = "";
disclaimerText.split(" ").forEach(word => {
const testLine = currentLine + word + " ";
if (ctx.measureText(testLine).width > maxDisclaimerWidth) {
disclaimerLines.push(currentLine.trim());
currentLine = word + " ";
} else {
currentLine = testLine;
}
});
disclaimerLines.push(currentLine.trim());
disclaimerLines.forEach(line => {
if (disclaimerCurrentY > permitHeight - padding - 50) return; // Stop if running out of space
ctx.fillText(line, textFieldsX, disclaimerCurrentY);
disclaimerCurrentY += 14; // Line height for disclaimer
});
}
// 7. Signature Line (Bottom Left Area)
const signatureAreaBottomMargin = 30;
const signatureTextY = permitHeight - padding - signatureAreaBottomMargin;
const signatureLineY = signatureTextY - 18;
const signatureLineStartX = photoBoxX;
const signatureLineWidth = (textFieldsX - 15) - photoBoxX; // Line ends before text fields start
ctx.strokeStyle = textColor;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(signatureLineStartX, signatureLineY);
ctx.lineTo(signatureLineStartX + signatureLineWidth, signatureLineY);
ctx.stroke();
ctx.font = `12px ${fontFamily}`;
ctx.textAlign = 'center';
ctx.fillText("(Traveler's Affirmation)", signatureLineStartX + signatureLineWidth / 2, signatureTextY);
// 8. Official Seal (Bottom Right Area)
const sealRadius = 40;
const sealMargin = 15; // Margin from border/padding
const sealX = permitWidth - padding - sealRadius - sealMargin;
const sealY = permitHeight - padding - sealRadius - sealMargin;
ctx.beginPath();
ctx.arc(sealX, sealY, sealRadius, 0, Math.PI * 2);
ctx.fillStyle = accentColor; // Seal background
ctx.fill();
// Decorative elements on the seal
ctx.strokeStyle = "#FFFFFFCC"; // Semi-transparent white for lines
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.arc(sealX, sealY, sealRadius * 0.85, 0, Math.PI * 2); // Inner circle 1
ctx.stroke();
ctx.beginPath();
ctx.arc(sealX, sealY, sealRadius * 0.60, 0, Math.PI * 2); // Inner circle 2
ctx.stroke();
// Text on seal
ctx.fillStyle = "#FFFFFF"; // White text for good contrast on dark seal
let sealFontSize = 18; // Default font size for seal text
if (officialSealText.length <= 2) sealFontSize = 22;
else if (officialSealText.length <= 3) sealFontSize = 18;
else sealFontSize = 14; // Smaller if text is longer
ctx.font = `bold ${sealFontSize}px ${fontFamily}`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(officialSealText, sealX, sealY);
ctx.textBaseline = 'alphabetic'; // Reset baseline
return canvas;
}
Apply Changes