You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg) {
// Using a static-like property on the function object to cache the font loading promise
// This ensures the font is fetched and processed only once, even if processImage is called multiple times.
if (typeof processImage._fontLoaderPromise === 'undefined') {
processImage._fontLoaderPromise = null;
}
async function _ensureFontLoadedInternal(fontFamily, fontUrl) {
// First, check if a font with this family name is already loaded and added by this mechanism
// This avoids issues if document.fonts.check() is true for a system font but not the specific webfont
if (processImage._fontLoaderPromise) {
const promiseState = await Promise.race([
processImage._fontLoaderPromise.then(status => ({ status: 'resolved', value: status })),
new Promise(resolve => setTimeout(() => resolve({ status: 'pending' }), 0)) // Check current state without re-triggering
]);
if (promiseState.status === 'resolved' && promiseState.value === true) {
// Check if the font object is indeed in document.fonts
for (const font of document.fonts.values()) {
if (font.family === fontFamily && font.status === 'loaded') {
return true; // Already loaded by a previous call successfully
}
}
// If promise resolved true but font not found, means something is off, reset promise
processImage._fontLoaderPromise = null;
} else if (promiseState.status === 'pending') {
return processImage._fontLoaderPromise; // Still loading, return the existing promise
}
// If promise resolved false, it will be reset below and retried.
}
// If no active promise, or if it previously failed and was reset
if (!processImage._fontLoaderPromise) {
processImage._fontLoaderPromise = new Promise(async (resolve) => {
try {
const font = new FontFace(fontFamily, `url(${fontUrl})`);
await font.load();
document.fonts.add(font);
resolve(true); // Font loaded successfully
} catch (e) {
console.error(`Failed to load font "${fontFamily}":`, e);
processImage._fontLoaderPromise = null; // Reset on failure to allow retry
resolve(false); // Font loading failed
}
});
}
return processImage._fontLoaderPromise;
}
// --- Constants ---
const FONT_FAMILY = 'Noto Sans Runic';
const FONT_URL = 'https://fonts.gstatic.com/s/notosansrunic/v21/H4ckBXKMC_BbOsYyL3gq841v0Yg_cnFfdw.woff2';
const FRAME_BASE_COLOR = '#959088'; // Stone gray
const FRAME_DARK_SPECKLE_COLOR = 'rgba(0, 0, 0, 0.1)';
const FRAME_LIGHT_SPECKLE_COLOR = 'rgba(255, 255, 255, 0.08)';
const RUNE_SHADOW_COLOR = '#403D39'; // Darker part of the "carved" rune
const RUNE_HIGHLIGHT_COLOR = '#B0ABA3'; // Lighter, "lit" edge of the carving
const INNER_SHADOW_COLOR = 'rgba(0, 0, 0, 0.35)';
const INNER_HIGHLIGHT_COLOR = 'rgba(255, 255, 255, 0.20)';
const BEVEL_WIDTH = 2; // pixels for inner bevel
const RUNES = ['ᚠ', 'ᚢ', 'ᚦ', 'ᚨ', 'ᚱ', 'ᚲ', 'ᚷ', 'ᚹ', 'ᚺ', 'ᚾ', 'ᛁ', 'ᛃ', 'ᛇ', 'ᛈ', 'ᛉ', 'ᛊ', 'ᛏ', 'ᛒ', 'ᛖ', 'ᛗ', 'ᛚ', 'ᛜ', 'ᛞ', 'ᛟ'];
// --- Font Loading ---
const fontLoaded = await _ensureFontLoadedInternal(FONT_FAMILY, FONT_URL);
// --- Calculate Dimensions ---
const imgWidth = originalImg.width;
const imgHeight = originalImg.height;
const minDim = Math.min(imgWidth, imgHeight);
const frameThickness = Math.max(35, Math.floor(minDim * 0.12)); // Min 35px thick frame
let runeSize = Math.max(14, Math.floor(frameThickness * 0.50));
if (runeSize % 2 !== 0) runeSize +=1; // Prefer even rune size for crispness/centering
const canvasWidth = imgWidth + 2 * frameThickness;
const canvasHeight = imgHeight + 2 * frameThickness;
// --- Canvas Setup ---
const canvas = document.createElement('canvas');
canvas.width = canvasWidth;
canvas.height = canvasHeight;
const ctx = canvas.getContext('2d');
// --- Helper: Draw Stone Texture on a Rect ---
function drawStoneTexture(x, y, w, h) {
ctx.fillStyle = FRAME_BASE_COLOR;
ctx.fillRect(x, y, w, h);
const numSpeckles = Math.floor((w * h) / 25); // Adjust density of speckles
for (let i = 0; i < numSpeckles; i++) {
const speckleX = x + Math.random() * w;
const speckleY = y + Math.random() * h;
const speckleRadius = Math.random() * 1.8 + 0.5; // Adjust speckle size
ctx.fillStyle = (Math.random() < 0.55) ? FRAME_DARK_SPECKLE_COLOR : FRAME_LIGHT_SPECKLE_COLOR;
ctx.beginPath();
ctx.arc(speckleX, speckleY, speckleRadius, 0, Math.PI * 2);
ctx.fill();
}
}
// --- Draw Stone Texture for Frame Parts ---
drawStoneTexture(0, 0, canvasWidth, frameThickness); // Top
drawStoneTexture(0, canvasHeight - frameThickness, canvasWidth, frameThickness); // Bottom
drawStoneTexture(0, frameThickness, frameThickness, imgHeight); // Left (main part)
drawStoneTexture(canvasWidth - frameThickness, frameThickness, frameThickness, imgHeight); // Right (main part)
// --- Draw Inner Bevel for the Image Area ---
ctx.fillStyle = INNER_SHADOW_COLOR;
ctx.fillRect(frameThickness, frameThickness, imgWidth, BEVEL_WIDTH); // Top shadow
ctx.fillRect(frameThickness, frameThickness + BEVEL_WIDTH, BEVEL_WIDTH, imgHeight - BEVEL_WIDTH); // Left shadow
ctx.fillStyle = INNER_HIGHLIGHT_COLOR;
ctx.fillRect(frameThickness + BEVEL_WIDTH, frameThickness + imgHeight - BEVEL_WIDTH, imgWidth - BEVEL_WIDTH, BEVEL_WIDTH); // Bottom highlight
ctx.fillRect(frameThickness + imgWidth - BEVEL_WIDTH, frameThickness, BEVEL_WIDTH, imgHeight - BEVEL_WIDTH); // Right highlight
// --- Draw the Original Image ---
ctx.drawImage(originalImg, frameThickness, frameThickness, imgWidth, imgHeight);
// --- Draw Runes ---
if (fontLoaded && runeSize >= 10 && frameThickness >= runeSize + 8 /* ensure padding */) {
ctx.font = `${runeSize}px "${FONT_FAMILY}"`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const runeSpacingFactor = 0.4; // Relative to runeSize
const runeVisualSpace = runeSize * (1 + runeSpacingFactor); // Space one rune takes including spacing
const highlightOffset = Math.max(1, Math.floor(runeSize * 0.06));
function drawEngravedRune(runeChar, x, y, rotation = 0) {
ctx.save();
ctx.translate(x, y);
ctx.rotate(rotation);
ctx.fillStyle = RUNE_HIGHLIGHT_COLOR; // Light catches this edge
ctx.fillText(runeChar, -highlightOffset, -highlightOffset);
ctx.fillStyle = RUNE_SHADOW_COLOR; // This is the main, darker part of the rune
ctx.fillText(runeChar, 0, 0);
ctx.restore();
}
// Horizontal Runes (Top & Bottom)
const horizontalRuneYTop = frameThickness / 2;
const horizontalRuneYBottom = canvasHeight - frameThickness / 2;
const horizontalStartOffset = frameThickness + runeVisualSpace / 2;
const horizontalEndOffset = canvasWidth - frameThickness - runeVisualSpace / 2;
for (let side = 0; side < 2; side++) { // 0 for top, 1 for bottom
const yPos = (side === 0) ? horizontalRuneYTop : horizontalRuneYBottom;
let currentX = horizontalStartOffset;
while (currentX <= horizontalEndOffset) {
drawEngravedRune(RUNES[Math.floor(Math.random() * RUNES.length)], currentX, yPos);
currentX += runeVisualSpace;
}
}
// Vertical Runes (Left & Right)
const verticalRuneXLeft = frameThickness / 2;
const verticalRuneXRight = canvasWidth - frameThickness / 2;
const verticalStartOffset = frameThickness + runeVisualSpace / 2;
const verticalEndOffset = canvasHeight - frameThickness - runeVisualSpace / 2;
for (let side = 0; side < 2; side++) { // 0 for left, 1 for right
const xPos = (side === 0) ? verticalRuneXLeft : verticalRuneXRight;
const rotation = (side === 0) ? Math.PI / 2 : -Math.PI / 2;
let currentY = verticalStartOffset;
while (currentY <= verticalEndOffset) {
drawEngravedRune(RUNES[Math.floor(Math.random() * RUNES.length)], xPos, currentY, rotation);
currentY += runeVisualSpace;
}
}
}
return canvas;
}
Apply Changes