Please bookmark this page to avoid losing your image tool!

Image Steampunk Document Frame 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, frameSizeRatio = 0.15, primaryMetalColor = "#8C7853", secondaryMetalColor = "#654321", rivetColor = "#707070", gearDensity = 2) {

    // --- Helper Functions ---

    function _hexToRgb(hex) {
        const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
        return result ? {
            r: parseInt(result[1], 16),
            g: parseInt(result[2], 16),
            b: parseInt(result[3], 16)
        } : { r: 0, g: 0, b: 0 }; // Default to black if parse fails
    }

    function _rgbToHex(r, g, b) {
        r = Math.max(0, Math.min(255, Math.round(r)));
        g = Math.max(0, Math.min(255, Math.round(g)));
        b = Math.max(0, Math.min(255, Math.round(b)));
        return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
    }

    function _adjustColor(hex, percent) {
        const { r, g, b } = _hexToRgb(hex);
        const amount = 255 * (percent / 100); // No Math.round here, allow float for precision with r,g,b
        const newR = r + amount;
        const newG = g + amount;
        const newB = b + amount;
        return _rgbToHex(newR, newG, newB); // _rgbToHex will clamp and round
    }

    function _lighten(hex, percent) { return _adjustColor(hex, percent); }
    function _darken(hex, percent) { return _adjustColor(hex, -percent); }

    function _drawRivet(ctx, cx, cy, radius, baseColor, highlightOffset = 0.3) {
        if (radius <= 0.5) return; // Do not draw tiny rivets
        const hr = radius * highlightOffset; // Highlight offset factor
        const grad = ctx.createRadialGradient(cx - hr, cy - hr, Math.max(0.1, radius * 0.1), cx, cy, radius);
        grad.addColorStop(0, _lighten(baseColor, 40));
        grad.addColorStop(0.5, baseColor);
        grad.addColorStop(1, _darken(baseColor, 30));
        
        ctx.beginPath();
        ctx.arc(cx, cy, radius, 0, Math.PI * 2);
        ctx.fillStyle = grad;
        ctx.fill();

        ctx.strokeStyle = _darken(baseColor, 50);
        ctx.lineWidth = Math.max(0.5, radius * 0.1);
        ctx.stroke();
    }

    function _drawGear(ctx, cx, cy, outerRadius, numTeeth, toothHeightRatio, gearPrimaryColor) {
        if (outerRadius <= 2) return; // Do not draw tiny gears
        const innerRadius = outerRadius * (1 - toothHeightRatio);
        if (innerRadius <= 0.5) return; 

        ctx.save();
        ctx.translate(cx, cy);

        ctx.beginPath();
        const angleIncrement = Math.PI / numTeeth;

        for (let i = 0; i < numTeeth * 2; i++) {
            const angle = i * angleIncrement;
            const r = (i % 2 === 0) ? outerRadius : innerRadius;
            if (i === 0) {
                ctx.moveTo(r * Math.cos(angle), r * Math.sin(angle));
            } else {
                ctx.lineTo(r * Math.cos(angle), r * Math.sin(angle));
            }
        }
        ctx.closePath();

        // Main gear body fill gradient
        const grad = ctx.createRadialGradient(-outerRadius * 0.3, -outerRadius * 0.3, outerRadius * 0.1, 0, 0, outerRadius);
        grad.addColorStop(0, _lighten(gearPrimaryColor, 20));
        grad.addColorStop(0.5, gearPrimaryColor);
        grad.addColorStop(1, _darken(gearPrimaryColor, 20));
        ctx.fillStyle = grad;
        ctx.fill();

        // Gear stroke
        ctx.strokeStyle = _darken(gearPrimaryColor, 40);
        ctx.lineWidth = Math.max(1, outerRadius * 0.03);
        ctx.stroke();

        // Central hole/decoration
        const holeRadius = Math.max(1, innerRadius * 0.5);
        if (holeRadius > 1) { // Only draw if reasonably sized
            ctx.beginPath();
            ctx.arc(0, 0, holeRadius, 0, Math.PI * 2, false);
            
            const holeGrad = ctx.createRadialGradient(holeRadius * 0.2, holeRadius * 0.2, 0, 0, 0, holeRadius);
            holeGrad.addColorStop(0, _darken(gearPrimaryColor, 10)); // Inner highlight of hole edge
            holeGrad.addColorStop(1, _darken(gearPrimaryColor, 60)); // Darker hole center
            ctx.fillStyle = holeGrad;
            ctx.fill();

            ctx.strokeStyle = _darken(gearPrimaryColor, 30);
            ctx.lineWidth = Math.max(0.5, outerRadius * 0.02);
            ctx.stroke();
        }
        ctx.restore();
    }

    // --- Main Processing ---

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

    const imgWidth = originalImg.naturalWidth || originalImg.width;
    const imgHeight = originalImg.naturalHeight || originalImg.height;

    if (imgWidth === 0 || imgHeight === 0) { // Handle case of invalid/unloaded image
        canvas.width = 200; canvas.height = 150; 
        ctx.fillStyle = "#CCCCCC";
        ctx.fillRect(0,0, canvas.width, canvas.height);
        ctx.font = "16px Arial";
        ctx.fillStyle = "#000000";
        ctx.textAlign = "center";
        ctx.fillText("Invalid Image Data", canvas.width/2, canvas.height/2);
        return canvas;
    }
    
    const baseBorderDim = Math.min(imgWidth, imgHeight);
    let borderWidth = baseBorderDim * frameSizeRatio;
    borderWidth = Math.max(20, borderWidth); // Ensure a minimum border width

    canvas.width = imgWidth + 2 * borderWidth;
    canvas.height = imgHeight + 2 * borderWidth;

    // 1. Parchment Background
    ctx.fillStyle = "#F5E8C8"; // Parchment color
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    
    ctx.save(); 
    ctx.globalAlpha = 0.6; // Make noise and splotches subtle
    // Noise
    for (let i = 0; i < (canvas.width * canvas.height) / 150; i++) { // Adjusted density
        const x = Math.random() * canvas.width;
        const y = Math.random() * canvas.height;
        const radius = Math.random() * 1.5 + 0.5;
        ctx.beginPath();
        ctx.arc(x, y, radius, 0, Math.PI * 2);
        ctx.fillStyle = `rgba(0, 0, 0, ${0.04 + Math.random() * 0.06})`; 
        ctx.fill();
    }
    // Splotches
    for (let i = 0; i < 15; i++) { // Adjusted number of splotches
        const splotchRadius = Math.random() * (Math.min(canvas.width, canvas.height) * 0.12) + (Math.min(canvas.width, canvas.height) * 0.04);
        const sx = Math.random() * (canvas.width - splotchRadius * 2) + splotchRadius;
        const sy = Math.random() * (canvas.height - splotchRadius * 2) + splotchRadius;

        const grad = ctx.createRadialGradient(sx, sy, 0, sx, sy, splotchRadius);
        const splotchBaseOpacity = 0.02 + Math.random() * 0.04; 
        grad.addColorStop(0, `rgba(160, 130, 90, ${splotchBaseOpacity})`); // Brownish tan
        grad.addColorStop(0.7, `rgba(160, 130, 90, ${splotchBaseOpacity * 0.5})`);
        grad.addColorStop(1, `rgba(160, 130, 90, 0)`);
        
        ctx.fillStyle = grad;
        ctx.fillRect(sx - splotchRadius, sy - splotchRadius, splotchRadius * 2, splotchRadius * 2);
    }
    ctx.restore(); 

    // 2. Main Metallic Frame Panels
    const frameSections = [
        { x: 0, y: 0, w: canvas.width, h: borderWidth }, // Top
        { x: 0, y: canvas.height - borderWidth, w: canvas.width, h: borderWidth }, // Bottom
        { x: 0, y: borderWidth, w: borderWidth, h: imgHeight }, // Left
        { x: canvas.width - borderWidth, y: borderWidth, w: borderWidth, h: imgHeight } // Right
    ];

    frameSections.forEach((panel, index) => {
        let grad;
        // Consistent light source from top-left
        if (panel.h < panel.w) { // Top or Bottom panel (horizontal)
            grad = ctx.createLinearGradient(panel.x, panel.y, panel.x, panel.y + panel.h); // Vertical gradient
            if (index === 0) { // Top panel
                grad.addColorStop(0, _lighten(primaryMetalColor, 20));
                grad.addColorStop(0.5, primaryMetalColor);
                grad.addColorStop(1, _darken(primaryMetalColor, 15));
            } else { // Bottom panel
                grad.addColorStop(0, _darken(primaryMetalColor, 15));
                grad.addColorStop(0.5, primaryMetalColor);
                grad.addColorStop(1, _lighten(primaryMetalColor, 20));
            }
        } else { // Left or Right panel (vertical)
            grad = ctx.createLinearGradient(panel.x, panel.y, panel.x + panel.w, panel.y); // Horizontal gradient
             if (index === 2) { // Left panel
                grad.addColorStop(0, _lighten(primaryMetalColor, 20));
                grad.addColorStop(0.5, primaryMetalColor);
                grad.addColorStop(1, _darken(primaryMetalColor, 15));
            } else { // Right panel
                grad.addColorStop(0, _darken(primaryMetalColor, 15));
                grad.addColorStop(0.5, primaryMetalColor);
                grad.addColorStop(1, _lighten(primaryMetalColor, 20));
            }
        }
        ctx.fillStyle = grad;
        ctx.fillRect(panel.x, panel.y, panel.w, panel.h);
        ctx.strokeStyle = _darken(primaryMetalColor, 35);
        ctx.lineWidth = Math.max(0.5, borderWidth * 0.01);
        ctx.strokeRect(panel.x, panel.y, panel.w, panel.h);
    });
    
    // Inner Bevel for frame opening (raised effect)
    const bevelWidth = Math.max(1.5, borderWidth * 0.04);
    ctx.strokeStyle = _lighten(primaryMetalColor, 30); // Highlight part
    ctx.lineWidth = bevelWidth;
    ctx.strokeRect(borderWidth - bevelWidth/2, borderWidth - bevelWidth/2, imgWidth + bevelWidth, imgHeight + bevelWidth);
    
    ctx.strokeStyle = _darken(primaryMetalColor, 30); // Shadow part
    ctx.lineWidth = bevelWidth;
    ctx.strokeRect(borderWidth + bevelWidth/2, borderWidth + bevelWidth/2, imgWidth - bevelWidth, imgHeight - bevelWidth);

    // 3. Draw the Original Image
    ctx.drawImage(originalImg, borderWidth, borderWidth, imgWidth, imgHeight);

    // 4. Add Rivets
    const rivetRadius = Math.max(2.5, borderWidth * 0.05);
    const effectiveRivetSpacing = Math.max(rivetRadius * 4, 25); 
    const rivetOffset = borderWidth * 0.5; // Center rivets in border thickness

    // Top Edge
    for (let x = rivetOffset; x <= canvas.width - rivetOffset; x += effectiveRivetSpacing) {
        _drawRivet(ctx, x, rivetOffset, rivetRadius, rivetColor);
    }
    // Bottom Edge
    for (let x = rivetOffset; x <= canvas.width - rivetOffset; x += effectiveRivetSpacing) {
        _drawRivet(ctx, x, canvas.height - rivetOffset, rivetRadius, rivetColor);
    }
    // Left Edge (avoid double-drawing corners)
    for (let y = rivetOffset + effectiveRivetSpacing; y <= canvas.height - rivetOffset - effectiveRivetSpacing; y += effectiveRivetSpacing) {
        _drawRivet(ctx, rivetOffset, y, rivetRadius, rivetColor);
    }
    // Right Edge (avoid double-drawing corners)
    for (let y = rivetOffset + effectiveRivetSpacing; y <= canvas.height - rivetOffset - effectiveRivetSpacing; y += effectiveRivetSpacing) {
        _drawRivet(ctx, canvas.width - rivetOffset, y, rivetRadius, rivetColor);
    }

    // 5. Add Gears
    if (gearDensity > 0) {
        const maxGearRadius = borderWidth * 0.40; 
        const minGearRadius = Math.max(5, borderWidth * 0.18); 
        
        let gearCandidates = [];
        // Define potential locations and properties for gears
        if (gearDensity >= 1) { // Basic corner gears
            gearCandidates.push(
                { cx: borderWidth * 0.5, cy: borderWidth * 0.5, sizeFactor: 1.0, metal: primaryMetalColor },
                { cx: canvas.width - borderWidth * 0.5, cy: borderWidth * 0.5, sizeFactor: 1.0, metal: secondaryMetalColor },
                { cx: borderWidth * 0.5, cy: canvas.height - borderWidth * 0.5, sizeFactor: 1.0, metal: secondaryMetalColor },
                { cx: canvas.width - borderWidth * 0.5, cy: canvas.height - borderWidth * 0.5, sizeFactor: 1.0, metal: primaryMetalColor }
            );
        }
        if (gearDensity >= 2) { // Add some offset/smaller gears
            gearCandidates.push(
                { cx: borderWidth * 1.1, cy: borderWidth * 0.45, sizeFactor: 0.65, metal: _lighten(primaryMetalColor,10)}, 
                { cx: canvas.width - borderWidth * 1.1, cy: canvas.height - borderWidth * 0.45, sizeFactor: 0.65, metal: _lighten(secondaryMetalColor,10)},
                { cx: borderWidth * 0.45, cy: canvas.height - borderWidth * 1.1, sizeFactor: 0.75, metal: _lighten(primaryMetalColor,5)},
                { cx: canvas.width - borderWidth * 0.45, cy: borderWidth * 1.1, sizeFactor: 0.75, metal: _lighten(secondaryMetalColor,5)}
            );
        }
        if (gearDensity >= 3) { // Add mid-frame gears
             gearCandidates.push(
                { cx: canvas.width * 0.5, cy: borderWidth * 0.4, sizeFactor: 0.85, metal: primaryMetalColor}, 
                { cx: canvas.width * 0.5, cy: canvas.height - borderWidth * 0.4, sizeFactor: 0.85, metal: secondaryMetalColor},
                { cx: borderWidth * 0.4, cy: canvas.height * 0.5, sizeFactor: 0.70, metal: primaryMetalColor}, 
                { cx: canvas.width - borderWidth * 0.4, cy: canvas.height * 0.5, sizeFactor: 0.70, metal: secondaryMetalColor}
            );
        }
        
        // Determine actual gears to draw based on density, avoid over-cluttering
        const gearsToDraw = [];
        const desiredGearCount = Math.min(gearCandidates.length, Math.max(0, gearDensity * 2 + (gearDensity > 0 ? 2 : 0) )); // e.g., D0:0, D1:4, D2:6, D3:8
        
        // Shuffle candidates to vary placement if desiredGearCount < gearCandidates.length
        // For deterministic results, we can take the first `desiredGearCount` items.
        for(let i = 0; i < desiredGearCount && i < gearCandidates.length; i++) {
            gearsToDraw.push(gearCandidates[i]);
        }

        gearsToDraw.forEach((loc) => {
            const radiusVariance = (0.8 + Math.random() * 0.4); // +/- 20% variance from base sizeFactor
            const radius = minGearRadius + (maxGearRadius - minGearRadius) * loc.sizeFactor * radiusVariance;
            const numTeeth = Math.floor(5 + Math.random() * 7); // 5 to 11 teeth
            const toothHeightRatio = 0.18 + Math.random() * 0.12; // 0.18 to 0.3 tooth height
            _drawGear(ctx, loc.cx, loc.cy, radius, numTeeth, toothHeightRatio, loc.metal);
        });
    }
    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 Steampunk Document Frame Creator is a web-based tool that allows users to transform their images into stylized steampunk-themed frames. By incorporating elements such as metallic borders, rivets, and gears, the tool enhances images with a unique retro-futuristic aesthetic. Users can customize the frame’s metallic colors, size, and additional decorative features, making it suitable for creating personalized artwork, enhancing presentation materials, or adding a creative touch to digital photos. This tool is perfect for artists, designers, and enthusiasts who want to explore a creative way to frame their images.

Leave a Reply

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