Please bookmark this page to avoid losing your image tool!

Victorian Séance Invitation Image 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, 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;
}

Free Image Tool Creator

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

Description

The Victorian Séance Invitation Image Creator is an online tool designed to craft beautifully themed invitations for a séance or similar events. Users can upload their own images and customize various textual elements including the event title, description, date, time, location, host details, RSVP information, and any additional notes. The tool provides a stylish, vintage-inspired design, featuring an elegant layout with ornate fonts and a classic background to enhance the atmospheric appeal of the invitation. This tool is perfect for creating unique invitations for themed parties, gatherings, or Halloween events, where a mystical or historical ambiance is desired.

Leave a Reply

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