Please bookmark this page to avoid losing your image tool!

Image Alchemy Manuscript 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,
    bgColor = "#FDF5E6",        // Page background color
    borderColor = "#8B4513",      // Page border color
    borderWidth = 20,              // Page border width
    textColor = "#4A3B31",         // Main text color
    imageFilter = "sepia(80%) brightness(0.95)", // CSS filter for the image
    imageFrame = "#5C4033",       // Image frame color
    imageFrameWidth = 5,           // Image frame width
    title = "Codex Arcanum",       // Title text
    body = "Incipit tractatus de lapide philosophorum. Materia prima, ex qua omnia constant, per artem nostram secreta revelat. Ignis et aqua, spiritus et corpus, in harmonia perfecta coniungantur. Cave lector, nam semita cognitionis ardua est et periclis plena.", // Body text
    fontName = "IM Fell English SC", // Font family name (requires fontUrl)
    fontUrl = "https://fonts.gstatic.com/s/imfellenglishsc/v17/a8IENpD3CDX-4_QHmkucFh4XcRUIxlBFncih.ttf", // URL for the font file
    titleSize = 30,                // Font size for title
    bodySize = 15,                 // Font size for body text
    symbolColor = "#600000",       // Dark red for symbols. #6A0DAD (purple) is also nice.
    symbolSize = 25,               // Size of alchemical symbols
    splotchColor = "#A0522D",      // Color of splotches/stains (Sienna)
    numSplotches = 8,              // Number of splotches
    numSymbols = 7                 // Number of decorative symbols
) {
    // Constants for canvas and layout
    const canvasWidth = 800;
    const canvasHeight = 1100;
    const bodyLineHeight = bodySize * 1.4;
    const contentPadding = borderWidth + 30; // Padding from canvas edge to content area
    const innerContentWidth = canvasWidth - 2 * contentPadding;

    // Create canvas
    const canvas = document.createElement('canvas');
    canvas.width = canvasWidth;
    canvas.height = canvasHeight;
    const ctx = canvas.getContext('2d');

    // --- Helper Functions (defined inside to be self-contained) ---

    async function loadWebFont(name, url) {
        if (!name || !url) return false;
        try {
            const font = new FontFace(name, `url(${url})`);
            await font.load();
            document.fonts.add(font);
            return true;
        } catch (e) {
            console.warn(`Failed to load font "${name}" from "${url}":`, e);
            return false;
        }
    }

    function drawAgedPaper(context, width, height, pageBgColor) {
        context.fillStyle = pageBgColor;
        context.fillRect(0, 0, width, height);

        const numParticles = Math.floor(width * height / 25); // Adjust density
        for (let i = 0; i < numParticles; i++) {
            const x = Math.random() * width;
            const y = Math.random() * height;
            const alpha = Math.random() * 0.12 + 0.03; // Slightly more visible particles
            const shade = (Math.random() - 0.5) * 40; // +/- 20 shade variation

            let r = parseInt(pageBgColor.substring(1, 3), 16);
            let g = parseInt(pageBgColor.substring(3, 5), 16);
            let b = parseInt(pageBgColor.substring(5, 7), 16);

            r = Math.min(255, Math.max(0, r + shade));
            g = Math.min(255, Math.max(0, g + shade));
            b = Math.min(255, Math.max(0, b + shade));

            context.fillStyle = `rgba(${r}, ${g}, ${b}, ${alpha})`;
            context.fillRect(x, y, Math.random() * 2 + 1, Math.random() * 2 + 1);
        }
    }
    
    function drawSplotch(context, cx, cy, maxR, baseSplotchColor) {
        const numCircles = Math.floor(Math.random() * 5) + 4; // 4 to 8 circles per splotch
        for (let i = 0; i < numCircles; i++) {
            const radius = Math.random() * maxR * 0.7 + maxR * 0.3; // Ensure splotch parts have substance
            const offsetX = (Math.random() - 0.5) * radius * 0.9; // Allow circles to offset
            const offsetY = (Math.random() - 0.5) * radius * 0.9;
            const alpha = Math.random() * 0.18 + 0.05; // Splotches can be a bit more prominent

            let r = parseInt(baseSplotchColor.substring(1, 3), 16);
            let g = parseInt(baseSplotchColor.substring(3, 5), 16);
            let b = parseInt(baseSplotchColor.substring(5, 7), 16);
            const variation = (Math.random() - 0.5) * 60; // Color variation within splotch
            r = Math.max(0, Math.min(255, r + variation));
            g = Math.max(0, Math.min(255, g + variation * 0.8)); // Less green variation for browns
            b = Math.max(0, Math.min(255, b + variation * 0.5)); // Even less blue variation

            context.fillStyle = `rgba(${r},${g},${b}, ${alpha})`;
            context.beginPath();
            context.arc(cx + offsetX, cy + offsetY, radius, 0, 2 * Math.PI);
            context.fill();
        }
    }

    function wrapText(context, textToWrap, x, y, maxWidth, lineHeight, fontStyle, txtColor) {
        context.font = fontStyle;
        context.fillStyle = txtColor;
        const words = textToWrap.split(' ');
        let line = '';
        let currentYPos = y;

        for (let n = 0; n < words.length; n++) {
            const testLine = line + words[n] + ' ';
            const metrics = context.measureText(testLine);
            const testWidth = metrics.width;
            if (testWidth > maxWidth && n > 0) {
                context.fillText(line.trim(), x, currentYPos);
                line = words[n] + ' ';
                currentYPos += lineHeight;
            } else {
                line = testLine;
            }
        }
        context.fillText(line.trim(), x, currentYPos);
        return currentYPos + lineHeight; // Return Y position after the last line of text
    }

    function drawAlchemicalSymbol(context, type, x, y, s, symColor) {
        context.save();
        context.fillStyle = symColor;
        context.strokeStyle = symColor; // For symbols that might use stroke
        context.lineWidth = Math.max(1, s / 12); // Adjusted line width for fill/stroke balance

        context.beginPath();
        switch (type) {
            case 'circleDot': // Sun / Gold
                context.arc(x, y, s / 2, 0, 2 * Math.PI); // Outer circle
                context.stroke(); // Stroke the circle
                context.beginPath(); // Start new path for dot
                context.arc(x, y, s / 5.5, 0, 2 * Math.PI); // Center dot
                context.fill(); // Fill the dot
                break;
            case 'crescent': // Moon / Silver
                context.arc(x, y, s / 2, -Math.PI / 2, Math.PI / 2, false); // Outer part of crescent
                // Inner cut: using arc with offset center
                context.arc(x + s / 7, y, s / 2.2, Math.PI / 2 - 0.35, -Math.PI / 2 + 0.35, true); 
                context.closePath();
                context.fill();
                break;
            case 'triangleUp': // Fire / Sulfur
                context.moveTo(x, y - s / 2.1); // Point slightly adjusted
                context.lineTo(x + s * 0.45, y + s / 4.2); // Base points adjusted
                context.lineTo(x - s * 0.45, y + s / 4.2);
                context.closePath();
                context.fill();
                break;
            case 'triangleDown': // Water / Mercury
                context.moveTo(x, y + s / 2.1);
                context.lineTo(x + s * 0.45, y - s / 4.2);
                context.lineTo(x - s * 0.45, y - s / 4.2);
                context.closePath();
                context.fill();
                break;
            case 'cross': // Simple Cross (e.g., for Antimony)
                const armLength = s * 0.85; // Slightly longer arms
                const armWidth = s * 0.22; // Slightly thicker arms
                context.fillRect(x - armLength / 2, y - armWidth / 2, armLength, armWidth); // Horizontal
                context.fillRect(x - armWidth / 2, y - armLength / 2, armWidth, armLength); // Vertical
                break;
            case 'square': // Salt / Earth (variant representation)
                context.fillRect(x - s / 2.6, y - s / 2.6, s * 2 / 2.6, s * 2 / 2.6); // A solid square
                break;
        }
        context.restore();
    }

    // --- Main Drawing Logic ---

    // 0. Load Font
    const fontLoaded = await loadWebFont(fontName, fontUrl);
    const defaultSerif = "Times New Roman, Times, serif";
    const effectiveFontFamily = fontLoaded ? `"${fontName}", ${defaultSerif}` : defaultSerif;

    // 1. Background Paper
    drawAgedPaper(ctx, canvasWidth, canvasHeight, bgColor);

    // 2. Splotches
    for (let i = 0; i < numSplotches; i++) {
        const splotchX = Math.random() * canvasWidth;
        const splotchY = Math.random() * canvasHeight;
        const splotchRadius = (Math.random() * 0.5 + 0.5) * Math.min(canvasWidth, canvasHeight) * 0.07;
        drawSplotch(ctx, splotchX, splotchY, splotchRadius, splotchColor);
    }

    // 3. Page Border
    if (borderWidth > 0) {
        ctx.strokeStyle = borderColor;
        ctx.lineWidth = borderWidth;
        // Draw rect from center of line to get full width inside canvas
        ctx.strokeRect(borderWidth / 2, borderWidth / 2, canvasWidth - borderWidth, canvasHeight - borderWidth);
    }
    
    let currentY = contentPadding;

    // 4. Title
    if (title) {
        ctx.fillStyle = textColor;
        ctx.font = `italic ${titleSize}px ${effectiveFontFamily}`;
        ctx.textAlign = "center";
        ctx.fillText(title, canvasWidth / 2, currentY + titleSize * 0.8); // Adjusted baseline for better look
        currentY += titleSize * 1.2 + 20; 
    }

    // 5. Image
    const maxImgHeight = canvasHeight / 2.8; // Max height for the image, adjusted
    const scaledImg = {
        width: originalImg.naturalWidth || originalImg.width, // Ensure dimensons are available
        height: originalImg.naturalHeight || originalImg.height
    };

    // Scale image to fit content width and max height
    if (scaledImg.width > innerContentWidth) {
        const ratio = innerContentWidth / scaledImg.width;
        scaledImg.width = innerContentWidth;
        scaledImg.height *= ratio;
    }
    if (scaledImg.height > maxImgHeight) {
        const ratio = maxImgHeight / scaledImg.height;
        scaledImg.height = maxImgHeight;
        scaledImg.width *= ratio;
    }

    const imgX = (canvasWidth - scaledImg.width) / 2;
    const imgY = currentY;

    if (imageFrameWidth > 0) {
        ctx.fillStyle = imageFrame;
        ctx.fillRect(
            imgX - imageFrameWidth, 
            imgY - imageFrameWidth, 
            scaledImg.width + 2 * imageFrameWidth, 
            scaledImg.height + 2 * imageFrameWidth
        );
    }
    
    ctx.filter = imageFilter || 'none';
    try {
        ctx.drawImage(originalImg, imgX, imgY, scaledImg.width, scaledImg.height);
    } catch (e) {
        console.error("Error drawing original image:", e);
        ctx.filter = 'none'; // Reset filter if error occurs before
        ctx.fillStyle = "#DDD"; // Light grey placeholder
        ctx.fillRect(imgX, imgY, scaledImg.width, scaledImg.height);
        ctx.fillStyle = textColor; // Use main text color for error message
        ctx.textAlign = "center";
        ctx.font = `${bodySize*0.9}px ${effectiveFontFamily}`;
        wrapText(ctx, "Error displaying image.", imgX + scaledImg.width/2, imgY + scaledImg.height/2 - bodySize, scaledImg.width * 0.9, bodySize, ctx.font, textColor);
    }
    ctx.filter = 'none'; // Reset filter after drawing

    currentY += scaledImg.height + imageFrameWidth + 25; 

    // 6. Body Text
    let currentYAfterText = currentY;
    if (body) {
        ctx.textAlign = "left"; // Default for body text
        currentYAfterText = wrapText(ctx, body, contentPadding, currentY, innerContentWidth, bodyLineHeight, `${bodySize}px ${effectiveFontFamily}`, textColor);
    }

    // 7. Decorative Symbols
    const symbolTypes = ['circleDot', 'crescent', 'triangleUp', 'triangleDown', 'cross', 'square'];
    const numCornerSymbols = Math.min(numSymbols, 4); // Max 4 symbols in corners
    // Distance from actual canvas edge, for symbols within border or near it.
    const cornerOffset = borderWidth + symbolSize / 1.5; 

    for (let i = 0; i < numSymbols; i++) {
        const symType = symbolTypes[i % symbolTypes.length];
        let sx, sy;

        if (i < numCornerSymbols) { // Place first few symbols in corners gracefully
            if (i === 0) { sx = cornerOffset; sy = cornerOffset; } // Top-Left
            else if (i === 1) { sx = canvasWidth - cornerOffset; sy = cornerOffset; } // Top-Right
            else if (i === 2) { sx = cornerOffset; sy = canvasHeight - cornerOffset; } // Bottom-Left
            else { sx = canvasWidth - cornerOffset; sy = canvasHeight - cornerOffset; } // Bottom-Right
        } else { // Place remaining symbols randomly in available margin spaces
            const marginChoice = Math.random();
            const availableTopMargin = contentPadding - borderWidth - symbolSize - 10;
            const availableBottomMargin = canvasHeight - currentYAfterText - borderWidth - symbolSize - 10;
            const availableSideMargin = contentPadding - borderWidth - symbolSize - 10;

            if (marginChoice < 0.33 && availableTopMargin > 0) { // Top margin area
                sx = contentPadding + Math.random() * innerContentWidth;
                sy = borderWidth + Math.random() * availableTopMargin + symbolSize / 2;
            } else if (marginChoice < 0.66 && availableBottomMargin > 0) { // Bottom margin area
                sx = contentPadding + Math.random() * innerContentWidth;
                sy = currentYAfterText + Math.random() * availableBottomMargin + symbolSize / 2;
            } else if (availableSideMargin > 0) { // Side margins
                sx = (Math.random() < 0.5) ? 
                     borderWidth + Math.random() * availableSideMargin + symbolSize / 2 : 
                     canvasWidth - contentPadding + Math.random() * availableSideMargin + symbolSize / 2;
                sy = contentPadding + Math.random() * (currentYAfterText - contentPadding - symbolSize); // Place along text height
                if (sy < contentPadding) sy = contentPadding + symbolSize; // ensure not over title
            } else { // Fallback if no margin space (e.g. very dense page) - random on border line
                 sx = Math.random() > 0.5 ? borderWidth/2 : canvasWidth - borderWidth/2;
                 sy = Math.random() * canvasHeight;
            }
        }
        // Ensure symbols are mostly within canvas and visible
        sx = Math.max(symbolSize / 2 + 5, Math.min(canvasWidth - symbolSize / 2 - 5, sx));
        sy = Math.max(symbolSize / 2 + 5, Math.min(canvasHeight - symbolSize / 2 - 5, sy));

        drawAlchemicalSymbol(ctx, symType, sx, sy, symbolSize, symbolColor);
    }
    
    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 Alchemy Manuscript Page Creator is a web-based tool designed to transform images into beautifully styled manuscript pages that convey a vintage or alchemical aesthetic. Users can customize the background color, border, and text styles, while also incorporating decorative symbols and splotches for added visual interest. This tool is perfect for creating decorative pages for art projects, educational materials, or unique presentations. It allows for the combination of images and stylized text in a coherent layout, catering to artists, writers, and anyone looking to add a classic touch to their digital content.

Leave a Reply

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