Please bookmark this page to avoid losing your image tool!

Image Ancient Cult Ritual Document 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, cultName = "The Ancient Order", ritualTitle = "Sacred Rites of Passage", symbols = "๐Ÿ’€๐–คโ™’๏ธŽโ™ˆ๏ธŽโ™‹๏ธŽโ™Š๏ธŽโ™Œ๏ธŽโ™Ž๏ธŽโ™“๏ธŽโ™๏ธŽโ™๏ธŽโ™๏ธŽโ™‰๏ธŽโ™‘๏ธŽโœงโ›งโœนโ˜ฝโ˜พโŠ•โŠ—โŠ™", mainColor = "#3a2a1a", paperColor = "#e8d9c3", imageOpacity = 0.5) {

    // Helper function: Convert hex color to RGB object
    function hexToRgb(hex) {
        // Ensure hex is a string and valid
        if (typeof hex !== 'string' || !hex.startsWith('#') || (hex.length !== 4 && hex.length !== 7)) {
            console.warn("Invalid hex color:", hex, "defaulting to black.");
            return { r: 0, g: 0, b: 0 }; // Default to black if invalid
        }
        let shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
        hex = hex.replace(shorthandRegex, function(m, r, g, b) {
            return r + r + g + g + b + b;
        });
        const bigint = parseInt(hex.slice(1), 16);
        return { r: (bigint >> 16) & 255, g: (bigint >> 8) & 255, b: bigint & 255 };
    }

    // Helper function: Adjust color brightness (lighten or darken)
    function adjustColor(hex, amount) {
        let { r, g, b } = hexToRgb(hex);
        r = Math.max(0, Math.min(255, r + amount));
        g = Math.max(0, Math.min(255, g + amount));
        b = Math.max(0, Math.min(255, b + amount));
        const toHex = c => ('0' + Math.round(c).toString(16)).slice(-2);
        return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
    }

    // Helper function: Wrap text to fit a max width
    function wrapText(context, text, maxWidth) {
        const words = text.split(' ');
        const lines = [];
        if (words.length === 0) return lines;
        let currentLine = words[0];
        for (let i = 1; i < words.length; i++) {
            const word = words[i];
            const width = context.measureText(currentLine + " " + word).width;
            if (width < maxWidth && currentLine.length < 150) { // Added length limit for very long words
                currentLine += " " + word;
            } else {
                lines.push(currentLine);
                currentLine = word;
            }
        }
        lines.push(currentLine);
        return lines;
    }

    // 0. Load Font
    const fontName = "IM Fell DW Pica"; // An old-style font
    const fontUrl = `https://fonts.googleapis.com/css2?family=IM+Fell+DW+Pica:ital@0;1&display=swap`;
    try {
        if (typeof document !== 'undefined' && document.fonts && !document.fonts.check(`12px "${fontName}"`)) {
            const fontLink = document.createElement('link');
            fontLink.href = fontUrl;
            fontLink.rel = 'stylesheet';
            document.head.appendChild(fontLink);
            // Wait for font to load
            await document.fonts.load(`1em "${fontName}"`);
            await document.fonts.load(`italic 1em "${fontName}"`);
        }
    } catch (e) {
        console.warn(`Font "${fontName}" failed to load or font API unsupported, using fallback fonts.`, e);
    }
    
    // 1. Canvas Setup
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');

    const docWidth = 800;
    const docHeight = Math.round(docWidth * 1.414); // A-series paper aspect ratio
    canvas.width = docWidth;
    canvas.height = docHeight;

    // 2. Background - Aged Paper
    ctx.fillStyle = paperColor;
    ctx.fillRect(0, 0, docWidth, docHeight);

    // Texture (subtle noise)
    const numTexturePixels = Math.round((docWidth * docHeight) / 25); // Density of specks
    const paperRgb = hexToRgb(paperColor);
    for (let i = 0; i < numTexturePixels; i++) {
        const x = Math.random() * docWidth;
        const y = Math.random() * docHeight;
        const alpha = Math.random() * 0.08 + 0.02;
        const shadeVariation = Math.random() * 40 - 20; // +/- 20 variation from base
        const r = Math.max(0, Math.min(255, paperRgb.r + shadeVariation));
        const g = Math.max(0, Math.min(255, paperRgb.g + shadeVariation));
        const b = Math.max(0, Math.min(255, paperRgb.b + shadeVariation));
        ctx.fillStyle = `rgba(${Math.round(r)},${Math.round(g)},${Math.round(b)},${alpha})`;
        ctx.fillRect(x, y, Math.random() * 2 + 1, Math.random() * 2 + 1);
    }

    // Stains
    const numStains = 6 + Math.floor(Math.random() * 6);
    const stainColorBase = adjustColor(paperColor, -45); // Darker, desaturated paper color for stains
    const stainRgb = hexToRgb(stainColorBase);

    for (let i = 0; i < numStains; i++) {
        const x = Math.random() * docWidth;
        const y = Math.random() * docHeight;
        const radius = (Math.random() * 60 + 40) * (docWidth / 800);
        const grad = ctx.createRadialGradient(x, y, radius * 0.1, x, y, radius);
        grad.addColorStop(0, `rgba(${stainRgb.r}, ${stainRgb.g}, ${stainRgb.b}, ${Math.random() * 0.15 + 0.05})`);
        grad.addColorStop(0.7, `rgba(${stainRgb.r}, ${stainRgb.g}, ${stainRgb.b}, ${Math.random() * 0.05})`);
        grad.addColorStop(1, `rgba(${stainRgb.r}, ${stainRgb.g}, ${stainRgb.b}, 0)`);
        
        ctx.fillStyle = grad;
        ctx.beginPath();
        ctx.moveTo(x + radius * Math.cos(0), y + radius * Math.sin(0));
        const points = 5 + Math.floor(Math.random()*5);
        for(let j = 1; j <= points; j++) {
            const angle = (j/points) * Math.PI * 2;
            const irregularRadius = radius * (0.7 + Math.random()*0.6); 
            ctx.lineTo(x + irregularRadius * Math.cos(angle), y + irregularRadius * Math.sin(angle));
        }
        ctx.closePath();
        ctx.fill();
    }

    // 3. Process and Draw originalImg
    const imgCanvas = document.createElement('canvas');
    const imgCtx = imgCanvas.getContext('2d');

    const imgBoxWidthPercentage = 0.6;
    const imgBoxHeightPercentage = 0.35;
    const imgBoxX = docWidth * (1 - imgBoxWidthPercentage) / 2;
    const imgBoxY = docHeight * 0.28; 
    const imgBoxMaxW = docWidth * imgBoxWidthPercentage;
    const imgBoxMaxH = docHeight * imgBoxHeightPercentage;

    let iW = originalImg.width;
    let iH = originalImg.height;
    if (iW === 0 || iH === 0) { iW = 200; iH = 200; } // Default for potentially unloaded image

    const scaleFactor = Math.min(imgBoxMaxW / iW, imgBoxMaxH / iH, 1.5); // Allow slight upscale for tiny images

    const imgDrawWidth = Math.max(1, iW * scaleFactor); // Ensure min 1px
    const imgDrawHeight = Math.max(1, iH * scaleFactor);

    imgCanvas.width = imgDrawWidth;
    imgCanvas.height = imgDrawHeight;

    imgCtx.drawImage(originalImg, 0, 0, imgDrawWidth, imgDrawHeight);

    const imageData = imgCtx.getImageData(0, 0, imgDrawWidth, imgDrawHeight);
    const data = imageData.data;
    const mainRgbForTint = hexToRgb(mainColor);

    for (let j = 0; j < data.length; j += 4) {
        const r = data[j];
        const g = data[j + 1];
        const b = data[j + 2];
        const gray = 0.299 * r + 0.587 * g + 0.114 * b; // Luminosity
        
        // Tint towards mainColor and desaturate/dim
        let newR = gray * (mainRgbForTint.r / 128 * 0.4 + 0.6); 
        let newG = gray * (mainRgbForTint.g / 128 * 0.4 + 0.6);
        let newB = gray * (mainRgbForTint.b / 128 * 0.4 + 0.6);

        const dimFactor = 0.85; 
        data[j] = Math.min(255, newR) * dimFactor;
        data[j + 1] = Math.min(255, newG) * dimFactor;
        data[j + 2] = Math.min(255, newB) * dimFactor;
    }
    imgCtx.putImageData(imageData, 0, 0);

    ctx.globalAlpha = Math.max(0, Math.min(1, imageOpacity)); // Clamp opacity
    ctx.globalCompositeOperation = 'multiply'; 
    const finalImgX = imgBoxX + (imgBoxMaxW - imgDrawWidth) / 2;
    const finalImgY = imgBoxY + (imgBoxMaxH - imgDrawHeight) / 2;
    if (imgDrawWidth > 0 && imgDrawHeight > 0) {
        ctx.drawImage(imgCanvas, finalImgX, finalImgY);
    }
    ctx.globalAlpha = 1.0;
    ctx.globalCompositeOperation = 'source-over';

    // 4. Add Text & Symbols
    ctx.fillStyle = mainColor;
    const baseFontSize = docWidth / 32; 
    const mainFont = `"${fontName}", "Times New Roman", serif`; 

    // Cult Name
    ctx.font = `italic ${Math.round(baseFontSize * 1.3)}px ${mainFont}`;
    ctx.textAlign = 'center';
    let currentTextY = docHeight * 0.12;
    ctx.fillText(cultName, docWidth / 2, currentTextY);

    // Ritual Title
    ctx.font = `${Math.round(baseFontSize * 1.0)}px ${mainFont}`;
    currentTextY += baseFontSize * 2.0;
    ctx.fillText(ritualTitle, docWidth / 2, currentTextY);

    const ritualPhrases = [
       "Et lux in tenebris non lucet.", "Vocamus te, spiritus antiqui.", "Per sanguinem et umbram.",
       "Sigillum fractum est.", "Verba potentiae obscurae.", "Nox profunda, silentium sacrum.",
       "Infernalis potentia, emerge!", "Circulus protectionis ducitur.", "Sacrificium acceptum sit.",
       "Porta aperta est inter mundos.", "Clavis ad abyssum.", "In nomine innominabilis."
    ];
    ctx.font = `${Math.round(baseFontSize * 0.78)}px ${mainFont}`;
    ctx.textAlign = 'left';
    const textBlockWidth = docWidth * 0.75;
    const textBlockX = (docWidth - textBlockWidth) / 2;
    let paragraphY = Math.max(finalImgY + imgDrawHeight + baseFontSize * 2.5, currentTextY + baseFontSize * 3.5);

    const numParagraphs = 2 + Math.floor(Math.random() * 2);
    for (let i = 0; i < numParagraphs; i++) {
        if (paragraphY > docHeight - baseFontSize * 5) break; 
        let paragraphText = "";
        const numSentences = 2 + Math.floor(Math.random() * 3);
        for(let s=0; s<numSentences; s++) {
            paragraphText += ritualPhrases[Math.floor(Math.random() * ritualPhrases.length)] + " ";
        }
        
        const lines = wrapText(ctx, paragraphText.trim(), textBlockWidth);
        lines.forEach(line => {
            if (paragraphY < docHeight - baseFontSize * 2.5) {
                ctx.save();
                const jitterX = (Math.random() * 2 - 1) * (baseFontSize * 0.06); 
                const jitterY = (Math.random() * 2 - 1) * (baseFontSize * 0.06); 
                ctx.globalAlpha = 0.65 + Math.random() * 0.3; 
                ctx.fillText(line, textBlockX + jitterX, paragraphY + jitterY);
                ctx.restore();
                paragraphY += baseFontSize * 1.15; // Line height
            }
        });
        paragraphY += baseFontSize * 0.8; 
    }
    
    // Scatter Symbols
    ctx.font = `${Math.round(baseFontSize * 1.9)}px "Segoe UI Symbol", "Symbola", "Noto Sans Symbols", sans-serif`;
    const symbolChars = symbols.split('');
    const numDecorativeSymbols = 15 + Math.floor(Math.random() * 10);
    for (let i = 0; i < numDecorativeSymbols && symbolChars.length > 0; i++) {
        const sym = symbolChars[Math.floor(Math.random() * symbolChars.length)];
        let sx = Math.random() * docWidth;
        let sy = Math.random() * docHeight;
        const margin = docWidth * 0.05;
        if (Math.random() < 0.65) { 
            if (Math.random() < 0.5) { // Left/Right margins
                sx = Math.random() * margin + (Math.random() < 0.5 ? 0 : docWidth - margin - ctx.measureText(sym).width);
            } else  { // Top/Bottom margins
                sy = Math.random() * margin + (Math.random() < 0.5 ? baseFontSize*1.9 : docHeight - margin);
            }
        }
        
        ctx.save();
        ctx.fillStyle = mainColor; // Ensure symbols use mainColor
        ctx.globalAlpha = 0.1 + Math.random() * 0.25; // Faint symbols
        ctx.translate(sx, sy);
        ctx.rotate((Math.random() - 0.5) * 0.7); 
        ctx.fillText(sym, 0, 0);
        ctx.restore();
    }

    // 5. Final Aging Touches
    const vignetteColorRgb = hexToRgb(adjustColor(mainColor, -30)); 

    // Vignette
    const outerRadius = docWidth * 0.8;
    const innerRadius = docWidth * 0.25;
    const vignetteGrad = ctx.createRadialGradient(docWidth / 2, docHeight / 2, innerRadius, docWidth / 2, docHeight / 2, outerRadius);
    vignetteGrad.addColorStop(0, `rgba(${vignetteColorRgb.r},${vignetteColorRgb.g},${vignetteColorRgb.b},0)`);
    vignetteGrad.addColorStop(1, `rgba(${vignetteColorRgb.r},${vignetteColorRgb.g},${vignetteColorRgb.b},0.35)`);
    ctx.fillStyle = vignetteGrad;
    ctx.fillRect(0, 0, docWidth, docHeight);

    // Subtle "burnt" or worn edges
    ctx.save();
    ctx.globalCompositeOperation = 'multiply'; 
    const edgeDarkness = 0.2;
    const edgeWidth = Math.round(40 * (docWidth/800));
    const edgeColor = `rgba(${vignetteColorRgb.r},${vignetteColorRgb.g},${vignetteColorRgb.b},${edgeDarkness})`;
    const transparentEdge = `rgba(${vignetteColorRgb.r},${vignetteColorRgb.g},${vignetteColorRgb.b},0)`;
    
    let gradEdge = ctx.createLinearGradient(0,0,0,edgeWidth);
    gradEdge.addColorStop(0, edgeColor); gradEdge.addColorStop(1, transparentEdge);
    ctx.fillStyle = gradEdge; ctx.fillRect(0,0,docWidth,edgeWidth);
    
    gradEdge = ctx.createLinearGradient(0,docHeight-edgeWidth,0,docHeight);
    gradEdge.addColorStop(0, transparentEdge); gradEdge.addColorStop(1, edgeColor);
    ctx.fillStyle = gradEdge; ctx.fillRect(0,docHeight-edgeWidth,docWidth,edgeWidth);
    
    gradEdge = ctx.createLinearGradient(0,0,edgeWidth,0);
    gradEdge.addColorStop(0, edgeColor); gradEdge.addColorStop(1, transparentEdge);
    ctx.fillStyle = gradEdge; ctx.fillRect(0,0,edgeWidth,docHeight);
    
    gradEdge = ctx.createLinearGradient(docWidth-edgeWidth,0,docWidth,0);
    gradEdge.addColorStop(0, transparentEdge); gradEdge.addColorStop(1, edgeColor);
    ctx.fillStyle = gradEdge; ctx.fillRect(docWidth-edgeWidth,0,edgeWidth,docHeight);
    ctx.restore();

    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 Ancient Cult Ritual Document Creator allows users to create a visually-appealing document resembling ancient ritual texts. Users can upload an image, and combined with customizable elements such as a cult name, ritual title, and symbolic decorations, the tool generates a unique document with a vintage aesthetic. This tool is suitable for creative projects, event invitations, or themed parties, particularly in contexts focusing on mystical or ancient themes.

Leave a Reply

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