You can edit the below JavaScript code to customize the image tool.
Apply Changes
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;
}
Apply Changes