Please bookmark this page to avoid losing your image tool!

Image Occult Grimoire 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,
    pageBackgroundColor = "#f0e6d2", // Parchment-like
    stainColor = "#8c7853",          // Brownish stain
    stainIntensity = 0.6,            // 0 to 1
    imageFilter = "sepia",           // 'none', 'grayscale', 'sepia'
    imageBorderColor = "#3d2b1f",    // Dark brown
    imageBorderWidth = 5,            // Pixels
    grimoireFont = "MedievalSharp, cursive", // Font family (MedievalSharp will be loaded from Google Fonts)
    textColor = "#2a1f15",          // Dark, ink-like brown
    customText = "In nomine umbrae, arcana revelantur.", // Sample grimoire text
    symbolCount = 12,                // Number of random symbols
    textFontSize = 28,               // Font size for the customText
    pagePadding = 100                // Padding around the image for the page effect
) {

    // --- Helper Functions ---

    function hexToRgba(hex, alpha) {
        let r = 0, g = 0, b = 0;
        if (hex.length === 4) { // #RGB
            r = parseInt(hex[1] + hex[1], 16);
            g = parseInt(hex[2] + hex[2], 16);
            b = parseInt(hex[3] + hex[3], 16);
        } else if (hex.length === 7) { // #RRGGBB
            r = parseInt(hex.substring(1, 3), 16);
            g = parseInt(hex.substring(3, 5), 16);
            b = parseInt(hex.substring(5, 7), 16);
        }
        return `rgba(${r},${g},${b},${alpha})`;
    }

    function hexToRgbArray(hex) {
        let r = 0, g = 0, b = 0;
        if (hex.length === 4) {
            r = parseInt(hex[1] + hex[1], 16); g = parseInt(hex[2] + hex[2], 16); b = parseInt(hex[3] + hex[3], 16);
        } else if (hex.length === 7) {
            r = parseInt(hex.substring(1, 3), 16); g = parseInt(hex.substring(3, 5), 16); b = parseInt(hex.substring(5, 7), 16);
        }
        return [r, g, b];
    }

    async function loadWebFont(fontFamilyName, fontUrl) {
        if (document.fonts && !document.fonts.check(`12px ${fontFamilyName}`)) {
            const fontFace = new FontFace(fontFamilyName, `url(${fontUrl})`);
            try {
                await fontFace.load();
                document.fonts.add(fontFace);
                // console.log(`${fontFamilyName} font loaded.`);
            } catch (e) {
                console.error(`${fontFamilyName} font failed to load:`, e);
                // Fallback to generic font in font string will apply
            }
        }
    }

    function drawParchmentTexture(ctx, width, height, pBaseColor, pStainColor, pStainIntensity, pTextColor) {
        ctx.fillStyle = pBaseColor;
        ctx.fillRect(0, 0, width, height);

        // Subtle noise dots
        const numNoiseDots = Math.floor((width * height) / 40); // Density of noise
        const baseRgb = hexToRgbArray(pBaseColor);
        for (let i = 0; i < numNoiseDots; i++) {
            const x = Math.random() * width;
            const y = Math.random() * height;
            const radius = Math.random() * 1.2;
            const lightnessAdjust = (Math.random() - 0.5) * 25;
            ctx.fillStyle = `rgba(${Math.max(0, Math.min(255, baseRgb[0] + lightnessAdjust))}, ${Math.max(0, Math.min(255, baseRgb[1] + lightnessAdjust))}, ${Math.max(0, Math.min(255, baseRgb[2] + lightnessAdjust))}, ${0.1 + Math.random() * 0.25})`;
            ctx.beginPath();
            ctx.arc(x, y, radius, 0, Math.PI * 2);
            ctx.fill();
        }
        
        // Stains
        const numStains = Math.floor((5 + 25 * pStainIntensity) * (width * height / (800*600)) );
        for (let i = 0; i < numStains; i++) {
            const sx = Math.random() * width;
            const sy = Math.random() * height;
            const srMax = (Math.random() * 0.08 + 0.04) * Math.min(width, height);
            const sr1 = Math.random() * srMax * 0.3; 
            const sr2 = sr1 + Math.random() * srMax * 0.7 + srMax * 0.2; 
            
            const gradient = ctx.createRadialGradient(sx, sy, sr1, sx, sy, sr2);
            const stainOpacity = (0.05 + Math.random() * 0.25) * pStainIntensity;
            
            let currentStainColorHex = pStainColor;
            if (Math.random() < 0.15) { // Occasionally use a darker ink-like stain
                 currentStainColorHex = pTextColor;
            }

            gradient.addColorStop(0, hexToRgba(currentStainColorHex, stainOpacity * (0.6 + Math.random() * 0.4)));
            gradient.addColorStop(0.7, hexToRgba(currentStainColorHex, stainOpacity * 0.4 * (0.5 + Math.random() * 0.5)));
            gradient.addColorStop(1, hexToRgba(currentStainColorHex, 0));

            ctx.fillStyle = gradient;
            ctx.beginPath();
            if (ctx.ellipse) {
                 const randomAngle = Math.random() * Math.PI;
                 const randomRatio = 0.4 + Math.random() * 0.6;
                 ctx.ellipse(sx, sy, sr2, sr2 * randomRatio, randomAngle, 0, Math.PI * 2);
            } else {
                 ctx.arc(sx, sy, sr2, 0, Math.PI * 2);
            }
            ctx.fill();
        }
    }

    function applyImageFilterToContext(ctxToFilter, filterType) {
        if (filterType === 'none' || !filterType) return;
        const canvasToFilter = ctxToFilter.canvas;
        const imageData = ctxToFilter.getImageData(0, 0, canvasToFilter.width, canvasToFilter.height);
        const data = imageData.data;
        for (let i = 0; i < data.length; i += 4) {
            const r = data[i];
            const g = data[i + 1];
            const b = data[i + 2];
            let tr, tg, tb;

            if (filterType === 'grayscale') {
                tr = tg = tb = 0.299 * r + 0.587 * g + 0.114 * b;
            } else if (filterType === 'sepia') {
                tr = 0.393 * r + 0.769 * g + 0.189 * b;
                tg = 0.349 * r + 0.686 * g + 0.168 * b;
                tb = 0.272 * r + 0.534 * g + 0.131 * b;
            } else {
                tr = r; tg = g; tb = b; // No change for unknown filter
            }
            data[i] = Math.max(0, Math.min(255, tr));
            data[i + 1] = Math.max(0, Math.min(255, tg));
            data[i + 2] = Math.max(0, Math.min(255, tb));
        }
        ctxToFilter.putImageData(imageData, 0, 0);
    }

    function drawOccultSymbol(ctx, x, y, size, color) {
        ctx.strokeStyle = color;
        ctx.fillStyle = color; // Some symbols might fill
        ctx.lineWidth = Math.max(1, size / 12);
        ctx.beginPath();
        const type = Math.floor(Math.random() * 5);
        
        // Save context state for local transformations if needed
        ctx.save();
        ctx.translate(x, y); // Move origin to symbol location
        ctx.rotate((Math.random() - 0.5) * 0.3); // Slight random rotation

        switch (type) {
            case 0: // Circle with cross
                ctx.arc(0, 0, size / 2, 0, Math.PI * 2);
                ctx.stroke();
                ctx.beginPath();
                ctx.moveTo(-size / 2, 0); ctx.lineTo(size / 2, 0);
                ctx.moveTo(0, -size / 2); ctx.lineTo(0, size / 2);
                ctx.stroke();
                break;
            case 1: // Triangle with inner dot
                ctx.moveTo(0, -size / 2);
                ctx.lineTo(size * 0.433, size / 4);
                ctx.lineTo(-size * 0.433, size / 4);
                ctx.closePath();
                ctx.stroke();
                ctx.beginPath();
                ctx.arc(0, 0, size / 8, 0, Math.PI * 2); // Inner dot
                ctx.fill();
                break;
            case 2: // Abstract sigil-like lines
                ctx.moveTo((Math.random()-0.5) * size*0.8, (Math.random()-0.5) * size*0.8);
                for (let i = 0; i < 2 + Math.floor(Math.random()*2) ; i++) {
                    ctx.lineTo(
                        (Math.random() - 0.5) * size,
                        (Math.random() - 0.5) * size
                    );
                }
                ctx.stroke();
                break;
            case 3: // Crescent moon
                ctx.beginPath();
                ctx.arc(0, 0, size / 2, Math.PI * 0.25, Math.PI * 1.75);
                ctx.stroke();
                ctx.beginPath();
                ctx.arc(size / (5 + Math.random()*2) , 0, size / (2.2 + Math.random()*0.5), Math.PI * 0.25, Math.PI * 1.75);
                ctx.stroke();
                break;
            case 4: // Eye-like symbol
                ctx.ellipse(0, 0, size / 2, size / 3.5, 0, 0, Math.PI * 2);
                ctx.stroke();
                ctx.beginPath();
                ctx.arc(0, 0, size / 5, 0, Math.PI * 2); // Pupil
                ctx.fill();
                break;
        }
        ctx.restore(); // Restore context state
    }

    // --- Main Logic ---

    const primaryFontName = grimoireFont.split(',')[0].trim();
    if (primaryFontName === "MedievalSharp") { // Specific handling for this font
        await loadWebFont('MedievalSharp', 'https://fonts.gstatic.com/s/medievalsharp/v25/EvOZPkZyKoEE2sVlLsvx0339jEgXzhU.woff2');
    }
    // For other fonts, user must ensure they are available or rely on system fallbacks in `grimoireFont` string

    const actualPadding = Math.max(30, pagePadding);
    const imgWidth = originalImg.naturalWidth;
    const imgHeight = originalImg.naturalHeight;

    const canvasPageWidth = Math.max(400, imgWidth + actualPadding * 2);
    const canvasPageHeight = Math.max(600, imgHeight + actualPadding * 2);

    const mainCanvas = document.createElement('canvas');
    mainCanvas.width = canvasPageWidth;
    mainCanvas.height = canvasPageHeight;
    const ctx = mainCanvas.getContext('2d');

    // 1. Draw Page Background (Parchment, Stains)
    drawParchmentTexture(ctx, canvasPageWidth, canvasPageHeight, pageBackgroundColor, stainColor, stainIntensity, textColor);

    // 2. Process and Draw the Image
    const imgCanvas = document.createElement('canvas');
    imgCanvas.width = imgWidth;
    imgCanvas.height = imgHeight;
    const imgCtx = imgCanvas.getContext('2d');
    imgCtx.drawImage(originalImg, 0, 0, imgWidth, imgHeight);

    if (imageFilter !== 'none') {
        applyImageFilterToContext(imgCtx, imageFilter);
    }

    const imgDrawX = (canvasPageWidth - imgWidth) / 2;
    const imgDrawY = (canvasPageHeight - imgHeight) / 2;
    ctx.drawImage(imgCanvas, imgDrawX, imgDrawY);

    // 3. Draw Image Border
    if (imageBorderWidth > 0) {
        ctx.strokeStyle = imageBorderColor;
        ctx.lineWidth = imageBorderWidth;
        ctx.strokeRect(imgDrawX - imageBorderWidth / 2, imgDrawY - imageBorderWidth / 2, 
                       imgWidth + imageBorderWidth, imgHeight + imageBorderWidth);
    }
    
    // 4. Add Grimoire Text
    ctx.font = `${textFontSize}px ${grimoireFont}`;
    ctx.fillStyle = textColor;
    ctx.textAlign = 'center';
    
    // Calculate available width for text to wrap if needed
    const textMargin = actualPadding * 0.5;
    const maxTextWidth = canvasPageWidth - textMargin * 2;
    
    // Simple text wrapping (basic)
    const words = customText.split(' ');
    let line = '';
    let textY = canvasPageHeight - actualPadding * 0.8; // Start text near bottom margin
    const lineHeight = textFontSize * 1.4;

    // Collect lines to draw them centered vertically if multi-line
    const lines = [];
    for (let n = 0; n < words.length; n++) {
        const testLine = line + words[n] + ' ';
        const metrics = ctx.measureText(testLine);
        const testWidth = metrics.width;
        if (testWidth > maxTextWidth && n > 0) {
            lines.push(line.trim());
            line = words[n] + ' ';
        } else {
            line = testLine;
        }
    }
    lines.push(line.trim());

    textY = canvasPageHeight - (actualPadding / 2) - ((lines.length -1) * lineHeight) / 2 ; 
    // Adjust textY based on number of lines if starting from bottom up, or use fixed position

    if (lines.length === 1) { // single line text position
         textY = canvasPageHeight - actualPadding / 2;
         ctx.textBaseline = 'bottom';
         ctx.fillText(lines[0], canvasPageWidth / 2, textY);
    } else { // multi-line text position (simple top-down from a point)
        textY = canvasPageHeight - actualPadding * 0.8; // Start position for multi-line title
        if(textY < imgDrawY + imgHeight + lineHeight) textY = imgDrawY + imgHeight + lineHeight*2; // Ensure below image
        if(textY + (lines.length-1)*lineHeight > canvasPageHeight - textMargin/2 ) textY = canvasPageHeight - textMargin/2 - (lines.length-1)*lineHeight; //Ensure not off page

        ctx.textBaseline = 'top';
        for(let i = 0; i < lines.length; i++) {
            ctx.fillText(lines[i], canvasPageWidth / 2, textY + (i * lineHeight));
        }
    }


    // 5. Add Occult Symbols
    const symbolSize = textFontSize * 1.2; 
    for (let i = 0; i < symbolCount; i++) {
        let sx, sy, validPosition = false;
        let attempts = 0;
        while(!validPosition && attempts < 20) { // Try to find a good spot
            sx = Math.random() * (canvasPageWidth - symbolSize) + symbolSize / 2;
            sy = Math.random() * (canvasPageHeight - symbolSize) + symbolSize / 2;
            
            // Check if symbol is in margins (not overlapping central image too much)
            const margin = symbolSize * 0.5; // Buffer around image
            const isOutsideImageX = (sx < imgDrawX - margin || sx > imgDrawX + imgWidth + margin);
            const isOutsideImageY = (sy < imgDrawY - margin || sy > imgDrawY + imgHeight + margin);
            
            if (isOutsideImageX || isOutsideImageY) {
                validPosition = true;
            }
            // Also try to avoid text area roughly (simple check)
            if (sy > textY - lineHeight && sy < textY + lines.length * lineHeight && Math.abs(sx - canvasPageWidth/2) < maxTextWidth/2) {
                validPosition = false; 
            }
            attempts++;
        }
        if(validPosition) { // Draw if a position is found (or fallback to anywhere if too many tries)
             drawOccultSymbol(ctx, sx, sy, symbolSize, textColor);
        } else { // Fallback: draw it anyway, perhaps slightly smaller or more transparent
             drawOccultSymbol(ctx, sx, sy, symbolSize * 0.8, hexToRgba(textColor,0.7));
        }
    }

    return mainCanvas;
}

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 Occult Grimoire Page Creator is an online tool designed to transform your images into beautifully crafted grimoire pages featuring a vintage aesthetic. By applying a parchment-like background along with various customizable elements, users can create an evocative design that incorporates stains, borders, and unique occult symbols. Ideal for artists, writers, and enthusiasts of the mystical or occult genres, this tool allows for personalized text and specialized filters like sepia and grayscale. Whether for use in creative projects, illustrations, or to enhance storytelling, this tool offers a unique way to present images with a mystical flair.

Leave a Reply

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