You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
name = "Jax Cosmic",
planetOfOrigin = "Xylos Prime",
passportId = "",
dateOfIssue = "",
universeId = "Alpha-7 Quadrant",
species = "Stardust Nomad",
themeColor = "#0D47A1", // Deep blue
accentColor = "#FFCA28", // Amber/gold
textColor = "#FFFFFF",
labelColor = "#B0BEC5", // Light greyish blue
fontFamily = "Arial, 'Helvetica Neue', Helvetica, sans-serif",
titleText = "MULTIVERSE PASSPORT",
authorityText = "Galactic Concord Registry",
expiryYears = 10
) {
const canvas = document.createElement('canvas');
const W = 900;
const H = 568; // Aspect ratio similar to ID-3 card (85.60mm x 53.98mm)
canvas.width = W;
canvas.height = H;
const ctx = canvas.getContext('2d');
// Helper: Convert hex color to RGB object
function hexToRgb(hex) {
if (!hex || typeof hex !== 'string') return { r: 0, g: 0, b: 0 }; // Default for safety
const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, (m, r, g, b) => r + r + g + g + b + b);
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : { r: 0, g: 0, b: 0 }; // Default on parse error
}
// Parameter processing
let pId = passportId;
if (!pId) {
pId = "MVP-" + Math.random().toString(36).substring(2, 8).toUpperCase() + "-" + Math.floor(Math.random() * 900 + 100);
}
let issueDateObj;
if (!dateOfIssue) {
issueDateObj = new Date();
} else {
issueDateObj = new Date(dateOfIssue);
if (isNaN(issueDateObj.getTime())) { // Fallback for invalid date string
issueDateObj = new Date();
}
}
const issueDateStr = issueDateObj.toISOString().split('T')[0];
const expiryDateObj = new Date(issueDateObj);
expiryDateObj.setFullYear(expiryDateObj.getFullYear() + expiryYears);
const expiryDateStr = expiryDateObj.toISOString().split('T')[0];
const nameTrimmed = name.trim();
const nameParts = nameTrimmed ? nameTrimmed.split(/\s+/) : [];
const surname = nameParts.length > 1 ? nameParts.pop().toUpperCase() : (nameParts[0] || "TRAVELER").toUpperCase();
const givenNames = nameParts.join(" ").toUpperCase() || "UNKNOWN";
const issuingCode = authorityText.split(/\s+/).map(word => word[0]).join("").toUpperCase().substring(0,3) || "GCR";
// --- Drawing starts ---
// 1. Background Color
ctx.fillStyle = themeColor;
ctx.fillRect(0, 0, W, H);
// 2. Background Pattern (faint repeating code)
const rgbTextColor = hexToRgb(textColor);
if (rgbTextColor) { // Ensure textColor was valid for rgba
ctx.save();
const patternFont = `bold 20px ${fontFamily}`;
ctx.font = patternFont;
ctx.fillStyle = `rgba(${rgbTextColor.r}, ${rgbTextColor.g}, ${rgbTextColor.b}, 0.04)`; // Very faint
// Measure text once for pattern spacing
const textMetrics = ctx.measureText(issuingCode);
const patternTextWidth = textMetrics.width;
ctx.translate(W / 2, H / 2);
ctx.rotate(-Math.PI / 6); // Rotate the coordinate system
const patternStepX = patternTextWidth + 80;
const patternStepY = 60;
// Calculate loop bounds to cover rotated canvas
const maxExtent = Math.sqrt(W*W + H*H) / 2 + Math.max(patternStepX, patternStepY);
for (let y = -maxExtent; y < maxExtent; y += patternStepY) {
for (let x = -maxExtent; x < maxExtent; x += patternStepX) {
ctx.fillText(issuingCode, x, y);
}
}
ctx.restore();
}
const padding = 30;
// 4. Header Text
ctx.fillStyle = accentColor;
ctx.font = `bold 30px ${fontFamily}`;
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillText(titleText.toUpperCase(), W / 2, padding);
// Line below title
ctx.strokeStyle = accentColor;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(padding, padding + 40);
ctx.lineTo(W - padding, padding + 40);
ctx.stroke();
// --- Layout regions ---
const photoSectionX = padding;
const photoSectionY = padding + 55;
const photoWidth = 220;
const photoHeight = Math.floor(photoWidth * 1.25);
const dataSectionX = photoSectionX + photoWidth + 25;
const dataSectionY = photoSectionY;
const dataSectionWidth = W - dataSectionX - padding;
// 5. Photo
ctx.fillStyle = "#444444"; // Darker placeholder bg for photo area
ctx.fillRect(photoSectionX, photoSectionY, photoWidth, photoHeight);
ctx.strokeStyle = accentColor;
ctx.lineWidth = 2;
ctx.strokeRect(photoSectionX, photoSectionY, photoWidth, photoHeight);
if (originalImg && originalImg.complete && originalImg.naturalWidth > 0) {
const imgAR = originalImg.naturalWidth / originalImg.naturalHeight;
const photoAR = photoWidth / photoHeight;
let sWidth = originalImg.naturalWidth, sHeight = originalImg.naturalHeight;
let sx = 0, sy = 0;
if (imgAR > photoAR) {
sWidth = originalImg.naturalHeight * photoAR;
sx = (originalImg.naturalWidth - sWidth) / 2;
} else if (imgAR < photoAR) {
sHeight = originalImg.naturalWidth / photoAR;
sy = (originalImg.naturalHeight - sHeight) / 2;
}
// Draw image slightly inset from border
ctx.drawImage(originalImg, sx, sy, sWidth, sHeight, photoSectionX + 2, photoSectionY + 2, photoWidth - 4, photoHeight - 4);
} else {
ctx.fillStyle = labelColor; // Use labelColor for placeholder text
ctx.font = `14px ${fontFamily}`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText("Photo Area", photoSectionX + photoWidth / 2, photoSectionY + photoHeight / 2);
}
// 6. Emblem (Star) below photo
function drawStar(cx, cy, spikes, outerRadius, innerRadius) {
let rot = Math.PI / 2 * 3;
let x = cx;
let y = cy;
const step = Math.PI / spikes;
ctx.beginPath();
ctx.moveTo(cx, cy - outerRadius);
for (let i = 0; i < spikes; i++) {
x = cx + Math.cos(rot) * outerRadius;
y = cy + Math.sin(rot) * outerRadius;
ctx.lineTo(x, y);
rot += step;
x = cx + Math.cos(rot) * innerRadius;
y = cy + Math.sin(rot) * innerRadius;
ctx.lineTo(x, y);
rot += step;
}
ctx.lineTo(cx, cy - outerRadius);
ctx.closePath();
ctx.fillStyle = accentColor;
ctx.fill();
}
const emblemSize = 25;
const emblemY = photoSectionY + photoHeight + padding + emblemSize / 2;
drawStar(photoSectionX + photoWidth / 2, emblemY, 5, emblemSize, emblemSize / 2.5);
const sigLineY = emblemY + emblemSize / 2 + 15;
if (sigLineY < H - padding) {
ctx.strokeStyle = labelColor;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(photoSectionX, sigLineY);
ctx.lineTo(photoSectionX + photoWidth, sigLineY);
ctx.stroke();
ctx.fillStyle = labelColor;
ctx.font = `italic 10px ${fontFamily}`;
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillText("SIGNATURE OF BEARER", photoSectionX + photoWidth/2, sigLineY + 3);
}
// 7. Data Fields
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
const labelFontBaseSize = 10;
const valueFontBaseSize = 14;
const labelFont = `${labelFontBaseSize}px ${fontFamily}`;
const valueFont = `bold ${valueFontBaseSize}px ${fontFamily}`;
const entrySpacing = Math.max(18, valueFontBaseSize + 6); // Vertical space between full entries
function drawDataField(label, value, x, y, width) {
ctx.font = labelFont;
ctx.fillStyle = labelColor;
ctx.fillText(label.toUpperCase(), x, y);
const labelHeight = labelFontBaseSize;
ctx.font = valueFont;
ctx.fillStyle = textColor;
ctx.fillText(value, x, y + labelHeight + 2, width); // Max width for value
const valueHeight = valueFontBaseSize;
return y + labelHeight + valueHeight + entrySpacing;
}
const smallLabelFontBaseSize = 9;
const smallValueFontBaseSize = 12;
const smallLabelFont = `${smallLabelFontBaseSize}px ${fontFamily}`;
const smallValueFont = `${smallValueFontBaseSize}px ${fontFamily}`;
let currentY = dataSectionY;
const thirdWidth = Math.floor(dataSectionWidth / 3) - 5; // Space between these 3 fields
// Row 1: Type, Code, Passport No.
ctx.font = smallLabelFont; ctx.fillStyle = labelColor;
ctx.fillText("TYPE", dataSectionX, currentY);
ctx.fillText("CODE", dataSectionX + thirdWidth + 5, currentY); // +5 for small gap
ctx.fillText("PASSPORT NO.", dataSectionX + 2 * (thirdWidth + 5), currentY);
currentY += smallLabelFontBaseSize + 2;
ctx.font = smallValueFont; ctx.fillStyle = textColor;
ctx.fillText("P", dataSectionX, currentY);
ctx.fillText(issuingCode, dataSectionX + thirdWidth + 5, currentY);
ctx.fillText(pId, dataSectionX + 2 * (thirdWidth + 5), currentY, thirdWidth);
currentY += smallValueFontBaseSize + entrySpacing;
// Subsequent fields
currentY = drawDataField("SURNAME", surname, dataSectionX, currentY, dataSectionWidth);
currentY = drawDataField("GIVEN NAMES", givenNames, dataSectionX, currentY, dataSectionWidth);
currentY = drawDataField("SPECIES", species, dataSectionX, currentY, dataSectionWidth);
currentY = drawDataField("PLANET OF ORIGIN", planetOfOrigin, dataSectionX, currentY, dataSectionWidth);
currentY = drawDataField("UNIVERSE ID", universeId, dataSectionX, currentY, dataSectionWidth);
currentY = drawDataField("DATE OF ISSUE", issueDateStr, dataSectionX, currentY, dataSectionWidth);
currentY = drawDataField("DATE OF EXPIRY", expiryDateStr, dataSectionX, currentY, dataSectionWidth);
currentY = drawDataField("AUTHORITY", authorityText, dataSectionX, currentY, dataSectionWidth);
// Mock MRZ (Machine Readable Zone)
const mrzY = H - padding - 26;
if (currentY < mrzY - 20) { // Only if there's space
const mrzFont = `14px "Courier New", Courier, monospace`;
ctx.font = mrzFont;
ctx.fillStyle = textColor; // MRZ usually black on light, or light on dark
ctx.textBaseline = 'alphabetic'; // More precise for MRZ line spacing
const mrzX = photoSectionX; // Align MRZ with photo section left Edge for full width effect
const mrzWidth = W - photoSectionX - padding; // MRZ extends over data and photo areas
const M_Metrics = ctx.measureText("M"); // Approx width of one char
const charsPerLine = Math.floor(mrzWidth / M_Metrics.width);
let mrzLine1 = `P<${issuingCode}${surname.replace(/\s/g, '<')}<<${givenNames.replace(/\s/g, '<')}`;
mrzLine1 = mrzLine1.substring(0, charsPerLine).padEnd(charsPerLine, '<').toUpperCase();
ctx.fillText(mrzLine1, mrzX, mrzY);
let mrzLine2 = `${pId.replace(/-/g, '')}<${Math.floor(Math.random()*9)}${universeId.substring(0,3).toUpperCase().padEnd(3,'X')}${issueDateStr.substring(2,4)}${issueDateStr.substring(5,7)}${issueDateStr.substring(8,10)}${Math.floor(Math.random()*9)}`;
mrzLine2 += `<${expiryDateStr.substring(2,4)}${expiryDateStr.substring(5,7)}${expiryDateStr.substring(8,10)}${Math.floor(Math.random()*9)}`;
// Add some random filler and a fake checksum digit for visual effect
let checksumFiller = "";
for(let i=0; i<10; i++) checksumFiller += Math.floor(Math.random()*10);
mrzLine2 = (mrzLine2 + checksumFiller).substring(0, charsPerLine -1) + Math.floor(Math.random()*10);
mrzLine2 = mrzLine2.padEnd(charsPerLine, '<').toUpperCase();
ctx.fillText(mrzLine2, mrzX, mrzY + (parseInt(mrzFont.match(/\d+/)[0],10) || 14) + 2);
}
return canvas;
}
Apply Changes