Please bookmark this page to avoid losing your image tool!

Image Treasure Hunter’s Journal 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,
                            padding = 50,
                            sepiaIntensity = 0.7,
                            pageColor = "rgb(245, 230, 200)", // Parchment like
                            edgeRoughness = 15,
                            edgeDarkeningColor = "rgba(101, 67, 33, 0.5)", // Sienna like
                            numStains = 5,
                            stainBaseColor = "rgb(160, 110, 70)", // Brownish
                            textureIntensity = 0.5 // Range 0-1 for texture dot density
                           ) {

    const paperBaseWidth = originalImg.width + 2 * padding;
    const paperBaseHeight = originalImg.height + 2 * padding;

    const canvasMargin = Math.max(20, edgeRoughness * 1.5); // Ensure enough margin for effects
    const canvas = document.createElement('canvas');
    canvas.width = paperBaseWidth + 2 * canvasMargin;
    canvas.height = paperBaseHeight + 2 * canvasMargin;
    const ctx = canvas.getContext('2d');
    ctx.clearRect(0, 0, canvas.width, canvas.height);

    const paperX = canvasMargin;
    const paperY = canvasMargin;

    // Helper: Parse RGB/A or Hex color string
    function parseColor(colorStr) {
        if (!colorStr) return { r: 0, g: 0, b: 0, a: 0 }; // transparent black if no color
        let r, g, b, a = 1;
        const input = String(colorStr).trim().toLowerCase();

        if (input.startsWith('#')) {
            if (input.length === 7) { // #RRGGBB
                r = parseInt(input.substring(1, 3), 16);
                g = parseInt(input.substring(3, 5), 16);
                b = parseInt(input.substring(5, 7), 16);
            } else if (input.length === 4) { // #RGB
                r = parseInt(input.substring(1, 2) + input.substring(1, 2), 16);
                g = parseInt(input.substring(2, 3) + input.substring(2, 3), 16);
                b = parseInt(input.substring(3, 4) + input.substring(3, 4), 16);
            } else return { r: 0, g: 0, b: 0, a: 0 };
        } else if (input.startsWith('rgb')) {
            const partsMatch = input.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
            if (!partsMatch) return { r: 0, g: 0, b: 0, a: 0 };
            r = parseInt(partsMatch[1]);
            g = parseInt(partsMatch[2]);
            b = parseInt(partsMatch[3]);
            if (partsMatch[4] !== undefined) {
                a = parseFloat(partsMatch[4]);
            }
        } else {
             // Simple named colors (extend if needed)
            const namedColors = { "sienna": {r:160,g:82,b:45}, "brown": {r:165,g:42,b:42} };
            if (namedColors[input]) return {...namedColors[input], a:1};
            console.warn("Unsupported color format:", colorStr, "Defaulting to transparent black.");
            return { r: 0, g: 0, b: 0, a: 0 };
        }

        if (isNaN(r) || isNaN(g) || isNaN(b) || isNaN(a)) return { r: 0, g: 0, b: 0, a: 0 };
        return { r, g, b, a };
    }
    
    // Helper: Generate a rough path for the paper
    function createRoughPath(x, y, width, height, roughness, segmentsPerEdge = 20) {
        const path = new Path2D();
        const points = [];
        const r = Math.max(1, roughness); // Ensure roughness is at least 1 to avoid flat lines if 0

        // Top-left corner
        points.push({x: x + (Math.random()-0.5) * r, y: y + (Math.random()-0.5) * r});
        // Top edge
        for (let i = 1; i <= segmentsPerEdge; i++) {
            const t = i / segmentsPerEdge;
            points.push({
                x: x + t * width + (Math.random() - 0.5) * r * 0.8,
                y: y + (Math.random() - 0.5) * r
            });
        }
        // Top-right corner
        points.push({x: x + width + (Math.random()-0.5) * r, y: y + (Math.random()-0.5) * r});
        // Right edge
        for (let i = 1; i <= segmentsPerEdge; i++) {
            const t = i / segmentsPerEdge;
            points.push({
                x: x + width + (Math.random() - 0.5) * r,
                y: y + t * height + (Math.random() - 0.5) * r * 0.8
            });
        }
        // Bottom-right corner
        points.push({x: x + width + (Math.random()-0.5) * r, y: y + height + (Math.random()-0.5) * r});
        // Bottom edge
        for (let i = 1; i <= segmentsPerEdge; i++) {
            const t = i / segmentsPerEdge;
            points.push({
                x: x + width - (t * width) + (Math.random() - 0.5) * r * 0.8,
                y: y + height + (Math.random() - 0.5) * r
            });
        }
        // Bottom-left corner
        points.push({x: x + (Math.random()-0.5) * r, y: y + height + (Math.random()-0.5) * r});
        // Left edge
        for (let i = 1; i <= segmentsPerEdge; i++) {
            const t = i / segmentsPerEdge;
            points.push({
                x: x + (Math.random() - 0.5) * r,
                y: y + height - (t * height) + (Math.random() - 0.5) * r * 0.8
            });
        }
        
        path.moveTo(points[0].x, points[0].y);
        for(let i = 1; i < points.length; i++) {
            path.lineTo(points[i].x, points[i].y);
        }
        path.closePath();
        return path;
    }
    
    const paperPath = createRoughPath(paperX, paperY, paperBaseWidth, paperBaseHeight, edgeRoughness);

    // 1. Optional: Shadow for the paper
    if (edgeRoughness > 0) {
        ctx.save();
        ctx.shadowColor = 'rgba(0,0,0,0.35)';
        ctx.shadowBlur = edgeRoughness * 1.2;
        ctx.shadowOffsetX = edgeRoughness * 0.25;
        ctx.shadowOffsetY = edgeRoughness * 0.25;
        const parsedPageColorForShadow = parseColor(pageColor);
        ctx.fillStyle = `rgba(${parsedPageColorForShadow.r},${parsedPageColorForShadow.g},${parsedPageColorForShadow.b},1)`;
        ctx.fill(paperPath);
        ctx.restore();
    }

    // 2. Fill paper with base color
    const parsedPageColor = parseColor(pageColor);
    ctx.fillStyle = `rgb(${parsedPageColor.r},${parsedPageColor.g},${parsedPageColor.b})`;
    ctx.fill(paperPath);

    // 3. Clip to paper path for internal content
    ctx.save();
    ctx.clip(paperPath);

    // 3a. Paper Texture (Noise)
    if (parsedPageColor && textureIntensity > 0) {
        const numDots = Math.floor(paperBaseWidth * paperBaseHeight * 0.001 * textureIntensity);
        const baseDotAlpha = 0.08; 
        for (let i = 0; i < numDots ; i++) {
            const x = paperX + Math.random() * paperBaseWidth;
            const y = paperY + Math.random() * paperBaseHeight;
            const r_off = (Math.random() - 0.5) * 35;
            const g_off = (Math.random() - 0.5) * 35;
            const b_off = (Math.random() - 0.5) * 35;
            const r = Math.max(0, Math.min(255, parsedPageColor.r + r_off));
            const g = Math.max(0, Math.min(255, parsedPageColor.g + g_off));
            const b = Math.max(0, Math.min(255, parsedPageColor.b + b_off));
            const dotAlphaVariation = (Math.random() * 0.5 + 0.5); // 0.5 to 1.0
            ctx.fillStyle = `rgba(${r},${g},${b},${baseDotAlpha * dotAlphaVariation})`;
            ctx.beginPath();
            ctx.arc(x, y, Math.random() * 1.5 + 0.5, 0, Math.PI * 2);
            ctx.fill();
        }
    }

    // 3b. Stains
    const rgbStainBase = parseColor(stainBaseColor);
    if (rgbStainBase && numStains > 0) {
        for (let k = 0; k < numStains; k++) {
            const stainCX = paperX + padding * 0.3 + Math.random() * (paperBaseWidth - padding * 0.6);
            const stainCY = paperY + padding * 0.3 + Math.random() * (paperBaseHeight - padding * 0.6);
            const stainMaxR = (Math.random() * 0.12 + 0.05) * Math.min(paperBaseWidth, paperBaseHeight);
            
            const numSegments = Math.floor(Math.random() * 4) + 3;
            for (let i = 0; i < numSegments; i++) {
                const segRadius = Math.random() * stainMaxR * 0.8 + stainMaxR * 0.2;
                const segX = stainCX + (Math.random() - 0.5) * stainMaxR * 0.8;
                const segY = stainCY + (Math.random() - 0.5) * stainMaxR * 0.8;
                
                const grad = ctx.createRadialGradient(segX, segY, segRadius * 0.05, segX, segY, segRadius);
                const alphaCenter = Math.random() * 0.15 + 0.05; 
                grad.addColorStop(0, `rgba(${rgbStainBase.r}, ${rgbStainBase.g}, ${rgbStainBase.b}, ${alphaCenter})`);
                grad.addColorStop(0.6, `rgba(${rgbStainBase.r}, ${rgbStainBase.g}, ${rgbStainBase.b}, ${alphaCenter * 0.35})`);
                grad.addColorStop(1, `rgba(${rgbStainBase.r}, ${rgbStainBase.g}, ${rgbStainBase.b}, 0)`);
                
                ctx.fillStyle = grad;
                ctx.beginPath();
                ctx.ellipse(segX, segY, 
                            segRadius * (0.7 + Math.random() * 0.6), 
                            segRadius * (0.7 + Math.random() * 0.6), 
                            Math.random() * Math.PI * 2, 0, Math.PI * 2);
                ctx.fill();
            }
        }
    }

    // 3c. Draw the Original Image
    ctx.save(); 
    if (sepiaIntensity > 0) {
        ctx.filter = `sepia(${Math.min(1, Math.max(0, sepiaIntensity))})`; // Clamp between 0 and 1
    }
    
    const imgDrawX = paperX + padding;
    const imgDrawY = paperY + padding;
    const imgDrawWidth = originalImg.width;
    const imgDrawHeight = originalImg.height;

    ctx.drawImage(originalImg, imgDrawX, imgDrawY, imgDrawWidth, imgDrawHeight);
    ctx.restore(); 

    ctx.restore(); // Remove clipping

    // 4. Darken/Rough up Edges of the Paper
    if (edgeRoughness > 0) {
        const parsedEdgeColor = parseColor(edgeDarkeningColor);
        if (parsedEdgeColor && parsedEdgeColor.a > 0) { // Only draw if color is somewhat visible
            ctx.strokeStyle = `rgba(${parsedEdgeColor.r},${parsedEdgeColor.g},${parsedEdgeColor.b},${parsedEdgeColor.a})`;
            ctx.lineWidth = Math.max(0.5, edgeRoughness * 0.1 + Math.random() * edgeRoughness * 0.15);
            ctx.lineCap = "round";
            ctx.lineJoin = "round";
            ctx.stroke(paperPath);

            // Add a second, slightly offset and more transparent stroke for depth
            ctx.lineWidth = Math.max(0.5, edgeRoughness * 0.2 + Math.random() * edgeRoughness * 0.2);
             ctx.strokeStyle = `rgba(${parsedEdgeColor.r},${parsedEdgeColor.g},${parsedEdgeColor.b},${parsedEdgeColor.a * 0.6})`;
            
            ctx.save();
            ctx.translate((Math.random()-0.5) * edgeRoughness * 0.05, (Math.random()-0.5) * edgeRoughness * 0.05);
            ctx.stroke(paperPath);
            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 Treasure Hunter’s Journal Creator is a versatile online tool that transforms images into an artistic, journal-like format. Users can customize various parameters such as sepia intensity, edge roughness, paper color, texture, and stain effects to achieve a unique, aged look for their images. This tool is ideal for creators looking to enhance digital scrapbooks, adventure logs, or treasure maps, giving them a vintage, treasure-hunting aesthetic. Whether for personal projects or artistic endeavors, this tool allows users to add depth and character to their visual storytelling.

Leave a Reply

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