Please bookmark this page to avoid losing your image tool!

Viking Runestone Photo 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, frameThickness = 50, stoneColor = "#A9A9A9", runeColor = "#404040", runesText = "ᚠᚢᚦᚨᚱᚲᚷᚹᚺᚾᛁᛃᛇᛈᛉᛊᛏᛒᛖᛗᛚᛜᛞᛟ") {

    const FONT_NAME = "VikingRunestoneFrameFont"; // Unique name for this tool's font
    const FONT_URL = "https://fonts.gstatic.com/s/norse/v19/flUQsetStatus--ATCR_Nw.woff2";
    let fontActuallyLoaded = false;

    // Helper to load the external runic font
    async function loadExternalFont(fontName, fontUrl) {
        // Check if font is already loaded and usable in the document
        if (document.fonts.check(`12px ${fontName}`)) {
            return true;
        }
        try {
            const fontFace = new FontFace(fontName, `url(${fontUrl})`);
            await fontFace.load(); // Wait for the font to download
            document.fonts.add(fontFace); // Add font to document.fonts
            // Check again to ensure it's now available
            return document.fonts.check(`12px ${fontName}`);
        } catch (e) {
            console.error(`Font loading failed for ${fontName} from ${fontUrl}:`, e);
            return false;
        }
    }

    // Helper to parse any CSS color string to an RGB object
    function getRGBFromColor(colorStr) {
        const tempCanvas = document.createElement('canvas');
        tempCanvas.width = tempCanvas.height = 1;
        // getContext with willReadFrequently for performance if this were called many times rapidly.
        const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true }); 
        tempCtx.fillStyle = colorStr; // Canvas API resolves named colors, hex, rgb(), etc.
        tempCtx.fillRect(0, 0, 1, 1);
        const colorData = tempCtx.getImageData(0, 0, 1, 1).data;
        return { r: colorData[0], g: colorData[1], b: colorData[2] };
    }

    // Helper to shade an RGB color (lighten or darken)
    function shadeRGBColor(r, g, b, percent) {
        const factor = 1 + percent / 100; // E.g., percent = -20 -> factor = 0.8 (darker)
        const newR = Math.max(0, Math.min(255, Math.round(r * factor)));
        const newG = Math.max(0, Math.min(255, Math.round(g * factor)));
        const newB = Math.max(0, Math.min(255, Math.round(b * factor)));
        return `rgb(${newR}, ${newG}, ${newB})`;
    }
    
    // Attempt to load the font (once per session ideally)
    fontActuallyLoaded = await loadExternalFont(FONT_NAME, FONT_URL);

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

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

    // Handle case with no frame
    if (frameThickness <= 0) {
        canvas.width = imgWidth;
        canvas.height = imgHeight;
        ctx.drawImage(originalImg, 0, 0, imgWidth, imgHeight);
        return canvas;
    }

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

    // 1. Draw Stone Background (base color)
    ctx.fillStyle = stoneColor;
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    // 2. Add Stone Texture (speckles)
    const stoneBaseRGB = getRGBFromColor(stoneColor);
    const numSpeckles = (canvas.width * canvas.height) / 25; // Adjust density as needed
    for (let i = 0; i < numSpeckles; i++) {
        const x = Math.random() * canvas.width;
        const y = Math.random() * canvas.height;
        const speckleRadius = Math.random() * 2.5 + 0.5; // Size of speckles
        
        const adjustment = (Math.random() - 0.5) * 70; // RGB variation: +/- 35
        const rVar = Math.max(0, Math.min(255, stoneBaseRGB.r + adjustment));
        const gVar = Math.max(0, Math.min(255, stoneBaseRGB.g + adjustment));
        const bVar = Math.max(0, Math.min(255, stoneBaseRGB.b + adjustment));

        ctx.fillStyle = `rgba(${Math.round(rVar)}, ${Math.round(gVar)}, ${Math.round(bVar)}, ${Math.random() * 0.5 + 0.15})`; // Opacity
        ctx.beginPath();
        ctx.arc(x, y, speckleRadius, 0, Math.PI * 2);
        ctx.fill();
    }

    // 3. Draw "Chiseled" Edges for image recess (depth effect)
    // These lines are drawn so their center is on the boundary of the image area.
    const bevelLineWidth = Math.max(1, Math.min(frameThickness * 0.1, 5)); // Scaled line width, e.g. max 5px
    ctx.lineWidth = bevelLineWidth;

    // Dark "shadow" lines (along top and left edge of image area on the frame)
    ctx.strokeStyle = shadeRGBColor(stoneBaseRGB.r, stoneBaseRGB.g, stoneBaseRGB.b, -20); // 20% darker
    ctx.beginPath();
    ctx.moveTo(frameThickness + imgWidth - bevelLineWidth / 2, frameThickness + bevelLineWidth / 2); 
    ctx.lineTo(frameThickness + bevelLineWidth / 2, frameThickness + bevelLineWidth / 2);            
    ctx.lineTo(frameThickness + bevelLineWidth / 2, frameThickness + imgHeight - bevelLineWidth / 2); 
    ctx.stroke();

    // Light "highlight" lines (bottom and right)
    ctx.strokeStyle = shadeRGBColor(stoneBaseRGB.r, stoneBaseRGB.g, stoneBaseRGB.b, 20); // 20% lighter
    ctx.beginPath();
    ctx.moveTo(frameThickness + bevelLineWidth / 2, frameThickness + imgHeight - bevelLineWidth / 2);            
    ctx.lineTo(frameThickness + imgWidth - bevelLineWidth / 2, frameThickness + imgHeight - bevelLineWidth / 2);
    ctx.lineTo(frameThickness + imgWidth - bevelLineWidth / 2, frameThickness + bevelLineWidth / 2);            
    ctx.stroke();


    // 4. Draw the Original Image
    ctx.drawImage(originalImg, frameThickness, frameThickness, imgWidth, imgHeight);

    // 5. Draw Runes
    const runeFontSize = Math.max(10, frameThickness * 0.55); // Min 10px font, e.g. 55% of frame thickness for runes
    
    // Only draw runes if text is provided and there's enough space (frame is thick enough for visible runes)
    if (runesText.trim() !== "" && frameThickness >= runeFontSize * 0.8 && frameThickness >= 15) {
        const runeChars = runesText.split('');
        
        function drawRunesOnStrip(stripStartX, stripCenterY_or_X, stripLength, isHorizontalLayout) {
            let currentPosOnStrip = runeFontSize * 0.5; // Start with padding from image edge
            const stripEndLimit = stripLength - runeFontSize * 0.5; // End with padding
            
            let runeCharIndex = Math.floor(Math.random() * runeChars.length); // Random start in rune string for variety

            ctx.font = `${runeFontSize}px ${fontActuallyLoaded ? FONT_NAME : 'serif'}`; // Use loaded font or fallback
            ctx.fillStyle = runeColor;
            ctx.textAlign = 'center';
            ctx.textBaseline = 'middle';

            // Carving effect using shadow: runes are dark, shadow implies light source from top-left, making runes appear 'sunk'
            const highlightColorForShadow = shadeRGBColor(stoneBaseRGB.r, stoneBaseRGB.g, stoneBaseRGB.b, 25); // Light highlight
            ctx.shadowColor = highlightColorForShadow;
            
            const shadowOffsetBase = Math.max(1, runeFontSize / 20); // Scale shadow offset with font size
            ctx.shadowOffsetX = shadowOffsetBase;
            ctx.shadowOffsetY = shadowOffsetBase;
            ctx.shadowBlur = shadowOffsetBase * 1.5; // Slightly more blur

            while (currentPosOnStrip < stripEndLimit && runeChars.length > 0) {
                if (runeCharIndex >= runeChars.length) runeCharIndex = 0; // Loop through runesText
                const charToDraw = runeChars[runeCharIndex];
                
                let textX, textY;
                if (isHorizontalLayout) {
                    textX = stripStartX + currentPosOnStrip; // stripStartX is the image-area's left edge for top/bottom strips
                    textY = stripCenterY_or_X;            // stripCenterY_or_X is Y-center of the horizontal frame part
                } else { // Vertical layout
                    textX = stripCenterY_or_X;            // stripCenterY_or_X is X-center of the vertical frame part
                    textY = stripStartX + currentPosOnStrip; // stripStartX is the image-area's top edge for left/right strips
                }
                ctx.fillText(charToDraw, textX, textY);
                
                // Advance position along the strip
                if (isHorizontalLayout) {
                    const charActualWidth = ctx.measureText(charToDraw).width;
                    currentPosOnStrip += charActualWidth + runeFontSize * 0.45; // Spacing: char width + 45% of font size
                } else { // Vertical layout
                    currentPosOnStrip += runeFontSize * 1.15; // Spacing: 115% of font size (covers char height + spacing)
                }
                runeCharIndex++;
            }

            // Reset shadow properties for subsequent drawing operations on canvas
            ctx.shadowOffsetX = 0;
            ctx.shadowOffsetY = 0;
            ctx.shadowBlur = 0;
            ctx.shadowColor = 'transparent';
        }

        // Draw runes on all four sides of the frame
        // Top strip (horizontal runes):
        drawRunesOnStrip(frameThickness, frameThickness / 2, imgWidth, true);
        // Bottom strip (horizontal runes):
        drawRunesOnStrip(frameThickness, canvas.height - frameThickness / 2, imgWidth, true);
        // Left strip (vertical runes):
        drawRunesOnStrip(frameThickness, frameThickness / 2, imgHeight, false);
        // Right strip (vertical runes):
        drawRunesOnStrip(frameThickness, canvas.width - frameThickness / 2, imgHeight, false);
    }

    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 Viking Runestone Photo Frame Creator is an online tool that allows users to enhance their images by adding a decorative frame designed to mimic stone runestones. Users can customize the frame’s thickness, color, and the design of runes that adorn the edges. This tool is perfect for those looking to create unique and artistic representations of their photos, making it suitable for personal projects, gifts, or enhancing social media images with a historic and themed flair.

Leave a Reply

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