Please bookmark this page to avoid losing your image tool!

Image Alchemist’s Formula Page 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,
    pageTitle = "Arcane Formulae",
    mainText = "Hic iacet draco absconditus, qui se ipsum devorat et regenerat.\n\nSolve et coagula, et invenies lapidem occultum. Materia prima ubique invenitur, sed ab stultis contemnitur.\n\nIn igne philosophico, nigredo in albedinem, et albedo in rubedinem transmutantur. Aurum nostrum non est aurum vulgi, sed spiritus vivus, qui omnia corpora penetrat et perficit.",
    fontFamily = "'Dancing Script', cursive",
    textColor = "#5D4037", /* Dark Brown */
    paperColor = "#F0E6D2", /* Pale Parchment */
    borderColor = "#8C7853", /* Darker Parchment */
    imageBorderColor = "#70543E" /* Darker brown for image border */
) {

    const FONT_TO_LOAD_NAME = 'Dancing Script';
    const FONT_TO_LOAD_URL = 'https://fonts.gstatic.com/s/dancingscript/v25/If2cXTr6YS-zF4S-kcSWSVi_sxjsohD9F楫5EW.woff2';
    let actualFontFamily = fontFamily;

    if (fontFamily.toLowerCase().includes('dancing script')) {
        let fontLoaded = false;
        try { // Check if font is already available
            if (document.fonts && typeof document.fonts.check === 'function') {
                 if (document.fonts.check(`12px "${FONT_TO_LOAD_NAME}"`)) {
                    fontLoaded = true;
                 }
            }
        } catch (e) { /* Ignore errors in checking */ }


        if (!fontLoaded && typeof FontFace === 'function') {
            const customFont = new FontFace(FONT_TO_LOAD_NAME, `url(${FONT_TO_LOAD_URL})`);
            try {
                await customFont.load();
                document.fonts.add(customFont);
                console.log(`${FONT_TO_LOAD_NAME} font loaded.`);
                actualFontFamily = `"${FONT_TO_LOAD_NAME}", ${fontFamily.split(',').slice(1).join(',') || 'cursive'}`;
            } catch (e) {
                console.error(`Font ${FONT_TO_LOAD_NAME} failed to load:`, e);
                actualFontFamily = fontFamily.split(',').slice(1).join(',') || 'cursive'; // Fallback
            }
        } else if (fontLoaded) {
             actualFontFamily = `"${FONT_TO_LOAD_NAME}", ${fontFamily.split(',').slice(1).join(',') || 'cursive'}`;
        } else { // FontFace API not supported or font not found
            actualFontFamily = fontFamily.split(',').slice(1).join(',') || 'cursive';
        }
    }


    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');

    const PAGE_WIDTH = 800;
    const PAGE_HEIGHT = 1100;
    canvas.width = PAGE_WIDTH;
    canvas.height = PAGE_HEIGHT;

    // 1. Draw Paper Background
    ctx.fillStyle = paperColor;
    ctx.fillRect(0, 0, PAGE_WIDTH, PAGE_HEIGHT);

    // Add subtle noise to paper
    for (let i = 0; i < 30000; i++) {
        const x = Math.random() * PAGE_WIDTH;
        const y = Math.random() * PAGE_HEIGHT;
        const grayShade = Math.random() * 50 + 200; // Light gray specks
        const alpha = Math.random() * 0.08;
        ctx.fillStyle = `rgba(${grayShade},${grayShade},${grayShade},${alpha})`;
        ctx.fillRect(x, y, Math.random()*2+1, Math.random()*2+1);
    }
    
    // Add subtle stains (simplified)
    function drawStain(cx, cy, baseR, baseG, baseB) {
        const numLayers = 5 + Math.floor(Math.random() * 5);
        const maxRadius = 40 + Math.random() * 60;
        for (let i = 0; i < numLayers; i++) {
            const radiusX = maxRadius * ( (numLayers - i) / numLayers ) * (0.7 + Math.random() * 0.6);
            const radiusY = maxRadius * ( (numLayers - i) / numLayers ) * (0.7 + Math.random() * 0.6);
            const alpha = (0.01 + Math.random() * 0.03) * (i / numLayers + 0.2);
            const r = Math.max(0, Math.min(255, baseR + Math.floor(Math.random() * 20) - 10));
            const g = Math.max(0, Math.min(255, baseG + Math.floor(Math.random() * 20) - 10));
            const b = Math.max(0, Math.min(255, baseB + Math.floor(Math.random() * 20) - 10));
            ctx.fillStyle = `rgba(${r},${g},${b},${alpha})`;
            ctx.beginPath();
            ctx.ellipse(
                cx + (Math.random() - 0.5) * 20, 
                cy + (Math.random() - 0.5) * 20, 
                radiusX, radiusY, 
                Math.random() * Math.PI * 2, 0, Math.PI * 2
            );
            ctx.fill();
        }
    }
    const stainBaseR = parseInt(borderColor.slice(1,3), 16); // Use borderColor as base for stains
    const stainBaseG = parseInt(borderColor.slice(3,5), 16);
    const stainBaseB = parseInt(borderColor.slice(5,7), 16);
    for(let k=0; k<5; k++) { // Draw a few stains randomly
        drawStain(Math.random()*PAGE_WIDTH, Math.random()*PAGE_HEIGHT, stainBaseR, stainBaseG, stainBaseB);
    }


    // 2. Draw Page Border
    const BORDER_MARGIN = 25;
    const BORDER_THICKNESS_OUTER = 8;
    const BORDER_THICKNESS_INNER = 1;

    ctx.strokeStyle = borderColor;
    ctx.lineWidth = BORDER_THICKNESS_OUTER;
    ctx.strokeRect(
        BORDER_MARGIN, BORDER_MARGIN,
        PAGE_WIDTH - 2 * BORDER_MARGIN, PAGE_HEIGHT - 2 * BORDER_MARGIN
    );
    ctx.strokeStyle = textColor;
    ctx.lineWidth = BORDER_THICKNESS_INNER;
    ctx.strokeRect(
        BORDER_MARGIN + BORDER_THICKNESS_OUTER / 2 + 2, BORDER_MARGIN + BORDER_THICKNESS_OUTER / 2 + 2,
        PAGE_WIDTH - 2 * (BORDER_MARGIN + BORDER_THICKNESS_OUTER / 2 + 2), PAGE_HEIGHT - 2 * (BORDER_MARGIN + BORDER_THICKNESS_OUTER / 2 + 2)
    );

    // Content margins
    const CONTENT_MARGIN_X = BORDER_MARGIN + BORDER_THICKNESS_OUTER + 20;
    const CONTENT_MARGIN_Y_TOP = BORDER_MARGIN + BORDER_THICKNESS_OUTER + 30;
    const CONTENT_MARGIN_Y_BOTTOM = BORDER_MARGIN + BORDER_THICKNESS_OUTER + 20;
    const CONTENT_WIDTH = PAGE_WIDTH - 2 * CONTENT_MARGIN_X;

    // 3. Draw Title
    ctx.fillStyle = textColor;
    ctx.textAlign = 'center';
    const titleFontSize = 52;
    ctx.font = `bold ${titleFontSize}px ${actualFontFamily}`;
    const titleY = CONTENT_MARGIN_Y_TOP + titleFontSize;
    ctx.fillText(pageTitle, PAGE_WIDTH / 2, titleY);
    
    let currentY = titleY + titleFontSize * 0.5; // Space after title

    // Decorative line
    currentY += 15;
    ctx.beginPath();
    ctx.moveTo(CONTENT_MARGIN_X + 50, currentY);
    ctx.lineTo(PAGE_WIDTH - CONTENT_MARGIN_X - 50, currentY);
    ctx.lineWidth = 0.5;
    ctx.strokeStyle = textColor;
    ctx.stroke();
    currentY += 25;


    // 4. Draw Image
    const maxImgWidth = CONTENT_WIDTH * 0.8;
    const maxImgHeight = PAGE_HEIGHT * 0.3;
    const imgAspect = originalImg.width / originalImg.height;
    
    let imgDrawWidth = maxImgWidth;
    let imgDrawHeight = imgDrawWidth / imgAspect;

    if (imgDrawHeight > maxImgHeight) {
        imgDrawHeight = maxImgHeight;
        imgDrawWidth = imgDrawHeight * imgAspect;
    }

    const imgX = (PAGE_WIDTH - imgDrawWidth) / 2;
    const imgY = currentY;

    ctx.save();
    ctx.filter = 'sepia(0.7) contrast(1.1) brightness(0.95) saturate(0.8)';
    ctx.drawImage(originalImg, imgX, imgY, imgDrawWidth, imgDrawHeight);
    ctx.restore();

    // Image border
    ctx.strokeStyle = imageBorderColor;
    ctx.lineWidth = 3;
    ctx.strokeRect(imgX - 1.5, imgY - 1.5, imgDrawWidth + 3, imgDrawHeight + 3); // outer slightly thicker
    ctx.strokeStyle = paperColor; // Inner "highlight" line to make it look inset
    ctx.lineWidth = 1;
    ctx.strokeRect(imgX + 2.5, imgY + 2.5, imgDrawWidth - 5, imgDrawHeight - 5);


    currentY = imgY + imgDrawHeight + 30; // Space after image

    // 5. Draw Main Text
    const textFontSize = 20;
    const lineHeight = textFontSize * 1.5;
    const paragraphIndentSpaces = 4; // Indent for first line of each paragraph
    ctx.font = `${textFontSize}px ${actualFontFamily}`;
    ctx.fillStyle = textColor;
    ctx.textAlign = 'left';

    function wrapAndDrawText(context, text, x, y, maxWidth, lineHeightVal, indentSpaces) {
        const paragraphs = text.split('\n\n'); // Split by double newline for paragraphs
        let localCurrentY = y;
        const indentString = ' '.repeat(indentSpaces);

        for (const paragraph of paragraphs) {
            if (paragraph.trim() === "") {
                localCurrentY += lineHeightVal;
                continue;
            }
            
            const words = paragraph.replace(/\n/g, ' ').split(' '); // Replace single newlines with space for flow
            let line = indentString;

            for (let n = 0; n < words.length; n++) {
                const testLine = line + words[n] + ' ';
                const metrics = context.measureText(testLine.trimEnd()); // Measure without trailing space
                const testWidth = metrics.width;

                if (testWidth > maxWidth && line !== indentString) {
                    context.fillText(line.trimEnd(), x, localCurrentY);
                    line = indentString + words[n] + ' ';
                    localCurrentY += lineHeightVal;
                } else {
                    line = testLine;
                }
            }
            context.fillText(line.trimEnd(), x, localCurrentY);
            localCurrentY += lineHeightVal; // Move to next line for new paragraph
        }
        return localCurrentY;
    }
    
    const textBlockX = CONTENT_MARGIN_X + 20;
    const textBlockWidth = CONTENT_WIDTH - 40;
    currentY = wrapAndDrawText(ctx, mainText, textBlockX, currentY, textBlockWidth, lineHeight, paragraphIndentSpaces);

    // 6. Draw Alchemical Symbols (optional footer decoration)
    currentY += 20; // Space before symbols
    if (currentY < PAGE_HEIGHT - CONTENT_MARGIN_Y_BOTTOM - 30) { // Only if space allows
        const symbols = ['☉', '☽', '☿', '♀', '♂', '♃', '♄', '△', '▽']; // Sun, Moon, Mercury, Venus, Mars, Jupiter, Saturn, Triangle, Inverted Triangle
        ctx.textAlign = 'center';
        ctx.font = `28px ${actualFontFamily}`; // Slightly larger than text
        let symbolString = "";
        for(let i=0; i<5; i++) {
            symbolString += symbols[Math.floor(Math.random() * symbols.length)] + "   ";
        }
        ctx.fillText(symbolString.trim(), PAGE_WIDTH / 2, currentY);
    }
    
    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 Alchemist’s Formula Page Creator is an online tool that allows users to transform images into beautifully styled pages reminiscent of ancient alchemical manuscripts. Users can input an image alongside customized page titles and main texts, applying decorative elements such as antique paper backgrounds, borders, and unique typography. This tool is ideal for crafting mystical-themed presentations, creating personalized artistic prints, or generating educational materials that require a visually captivating layout. Perfect for artists, educators, or anyone looking to infuse a sense of magic and creativity into their visual projects.

Leave a Reply

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