Please bookmark this page to avoid losing your image tool!

Image Multiverse Passport Creator

(Free & Supports Bulk Upload)

Drag & drop your images here or

The result will appear here...
You can edit the below JavaScript code to customize the image tool.
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;
}

Free Image Tool Creator

Can't find the image tool you're looking for?
Create one based on your own needs now!

Description

The Image Multiverse Passport Creator is a web tool designed to generate customizable digital passports featuring interstellar themes. Users can input personal details, such as name, planet of origin, and species, alongside a photo. The tool tailors the passport layout with options for colors, fonts, and design elements, including a machine-readable zone. Ideal for creative projects, gaming, or promotional content, this tool allows users to create visually distinctive passports that can be used for fun, fictional storytelling, or as a unique addition to craft projects and events.

Leave a Reply

Your email address will not be published. Required fields are marked *