You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
pageBackgroundColor = "#f0e6d2", // Parchment-like
stainColor = "#8c7853", // Brownish stain
stainIntensity = 0.6, // 0 to 1
imageFilter = "sepia", // 'none', 'grayscale', 'sepia'
imageBorderColor = "#3d2b1f", // Dark brown
imageBorderWidth = 5, // Pixels
grimoireFont = "MedievalSharp, cursive", // Font family (MedievalSharp will be loaded from Google Fonts)
textColor = "#2a1f15", // Dark, ink-like brown
customText = "In nomine umbrae, arcana revelantur.", // Sample grimoire text
symbolCount = 12, // Number of random symbols
textFontSize = 28, // Font size for the customText
pagePadding = 100 // Padding around the image for the page effect
) {
// --- Helper Functions ---
function hexToRgba(hex, alpha) {
let r = 0, g = 0, b = 0;
if (hex.length === 4) { // #RGB
r = parseInt(hex[1] + hex[1], 16);
g = parseInt(hex[2] + hex[2], 16);
b = parseInt(hex[3] + hex[3], 16);
} else if (hex.length === 7) { // #RRGGBB
r = parseInt(hex.substring(1, 3), 16);
g = parseInt(hex.substring(3, 5), 16);
b = parseInt(hex.substring(5, 7), 16);
}
return `rgba(${r},${g},${b},${alpha})`;
}
function hexToRgbArray(hex) {
let r = 0, g = 0, b = 0;
if (hex.length === 4) {
r = parseInt(hex[1] + hex[1], 16); g = parseInt(hex[2] + hex[2], 16); b = parseInt(hex[3] + hex[3], 16);
} else if (hex.length === 7) {
r = parseInt(hex.substring(1, 3), 16); g = parseInt(hex.substring(3, 5), 16); b = parseInt(hex.substring(5, 7), 16);
}
return [r, g, b];
}
async function loadWebFont(fontFamilyName, fontUrl) {
if (document.fonts && !document.fonts.check(`12px ${fontFamilyName}`)) {
const fontFace = new FontFace(fontFamilyName, `url(${fontUrl})`);
try {
await fontFace.load();
document.fonts.add(fontFace);
// console.log(`${fontFamilyName} font loaded.`);
} catch (e) {
console.error(`${fontFamilyName} font failed to load:`, e);
// Fallback to generic font in font string will apply
}
}
}
function drawParchmentTexture(ctx, width, height, pBaseColor, pStainColor, pStainIntensity, pTextColor) {
ctx.fillStyle = pBaseColor;
ctx.fillRect(0, 0, width, height);
// Subtle noise dots
const numNoiseDots = Math.floor((width * height) / 40); // Density of noise
const baseRgb = hexToRgbArray(pBaseColor);
for (let i = 0; i < numNoiseDots; i++) {
const x = Math.random() * width;
const y = Math.random() * height;
const radius = Math.random() * 1.2;
const lightnessAdjust = (Math.random() - 0.5) * 25;
ctx.fillStyle = `rgba(${Math.max(0, Math.min(255, baseRgb[0] + lightnessAdjust))}, ${Math.max(0, Math.min(255, baseRgb[1] + lightnessAdjust))}, ${Math.max(0, Math.min(255, baseRgb[2] + lightnessAdjust))}, ${0.1 + Math.random() * 0.25})`;
ctx.beginPath();
ctx.arc(x, y, radius, 0, Math.PI * 2);
ctx.fill();
}
// Stains
const numStains = Math.floor((5 + 25 * pStainIntensity) * (width * height / (800*600)) );
for (let i = 0; i < numStains; i++) {
const sx = Math.random() * width;
const sy = Math.random() * height;
const srMax = (Math.random() * 0.08 + 0.04) * Math.min(width, height);
const sr1 = Math.random() * srMax * 0.3;
const sr2 = sr1 + Math.random() * srMax * 0.7 + srMax * 0.2;
const gradient = ctx.createRadialGradient(sx, sy, sr1, sx, sy, sr2);
const stainOpacity = (0.05 + Math.random() * 0.25) * pStainIntensity;
let currentStainColorHex = pStainColor;
if (Math.random() < 0.15) { // Occasionally use a darker ink-like stain
currentStainColorHex = pTextColor;
}
gradient.addColorStop(0, hexToRgba(currentStainColorHex, stainOpacity * (0.6 + Math.random() * 0.4)));
gradient.addColorStop(0.7, hexToRgba(currentStainColorHex, stainOpacity * 0.4 * (0.5 + Math.random() * 0.5)));
gradient.addColorStop(1, hexToRgba(currentStainColorHex, 0));
ctx.fillStyle = gradient;
ctx.beginPath();
if (ctx.ellipse) {
const randomAngle = Math.random() * Math.PI;
const randomRatio = 0.4 + Math.random() * 0.6;
ctx.ellipse(sx, sy, sr2, sr2 * randomRatio, randomAngle, 0, Math.PI * 2);
} else {
ctx.arc(sx, sy, sr2, 0, Math.PI * 2);
}
ctx.fill();
}
}
function applyImageFilterToContext(ctxToFilter, filterType) {
if (filterType === 'none' || !filterType) return;
const canvasToFilter = ctxToFilter.canvas;
const imageData = ctxToFilter.getImageData(0, 0, canvasToFilter.width, canvasToFilter.height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
let tr, tg, tb;
if (filterType === 'grayscale') {
tr = tg = tb = 0.299 * r + 0.587 * g + 0.114 * b;
} else if (filterType === 'sepia') {
tr = 0.393 * r + 0.769 * g + 0.189 * b;
tg = 0.349 * r + 0.686 * g + 0.168 * b;
tb = 0.272 * r + 0.534 * g + 0.131 * b;
} else {
tr = r; tg = g; tb = b; // No change for unknown filter
}
data[i] = Math.max(0, Math.min(255, tr));
data[i + 1] = Math.max(0, Math.min(255, tg));
data[i + 2] = Math.max(0, Math.min(255, tb));
}
ctxToFilter.putImageData(imageData, 0, 0);
}
function drawOccultSymbol(ctx, x, y, size, color) {
ctx.strokeStyle = color;
ctx.fillStyle = color; // Some symbols might fill
ctx.lineWidth = Math.max(1, size / 12);
ctx.beginPath();
const type = Math.floor(Math.random() * 5);
// Save context state for local transformations if needed
ctx.save();
ctx.translate(x, y); // Move origin to symbol location
ctx.rotate((Math.random() - 0.5) * 0.3); // Slight random rotation
switch (type) {
case 0: // Circle with cross
ctx.arc(0, 0, size / 2, 0, Math.PI * 2);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(-size / 2, 0); ctx.lineTo(size / 2, 0);
ctx.moveTo(0, -size / 2); ctx.lineTo(0, size / 2);
ctx.stroke();
break;
case 1: // Triangle with inner dot
ctx.moveTo(0, -size / 2);
ctx.lineTo(size * 0.433, size / 4);
ctx.lineTo(-size * 0.433, size / 4);
ctx.closePath();
ctx.stroke();
ctx.beginPath();
ctx.arc(0, 0, size / 8, 0, Math.PI * 2); // Inner dot
ctx.fill();
break;
case 2: // Abstract sigil-like lines
ctx.moveTo((Math.random()-0.5) * size*0.8, (Math.random()-0.5) * size*0.8);
for (let i = 0; i < 2 + Math.floor(Math.random()*2) ; i++) {
ctx.lineTo(
(Math.random() - 0.5) * size,
(Math.random() - 0.5) * size
);
}
ctx.stroke();
break;
case 3: // Crescent moon
ctx.beginPath();
ctx.arc(0, 0, size / 2, Math.PI * 0.25, Math.PI * 1.75);
ctx.stroke();
ctx.beginPath();
ctx.arc(size / (5 + Math.random()*2) , 0, size / (2.2 + Math.random()*0.5), Math.PI * 0.25, Math.PI * 1.75);
ctx.stroke();
break;
case 4: // Eye-like symbol
ctx.ellipse(0, 0, size / 2, size / 3.5, 0, 0, Math.PI * 2);
ctx.stroke();
ctx.beginPath();
ctx.arc(0, 0, size / 5, 0, Math.PI * 2); // Pupil
ctx.fill();
break;
}
ctx.restore(); // Restore context state
}
// --- Main Logic ---
const primaryFontName = grimoireFont.split(',')[0].trim();
if (primaryFontName === "MedievalSharp") { // Specific handling for this font
await loadWebFont('MedievalSharp', 'https://fonts.gstatic.com/s/medievalsharp/v25/EvOZPkZyKoEE2sVlLsvx0339jEgXzhU.woff2');
}
// For other fonts, user must ensure they are available or rely on system fallbacks in `grimoireFont` string
const actualPadding = Math.max(30, pagePadding);
const imgWidth = originalImg.naturalWidth;
const imgHeight = originalImg.naturalHeight;
const canvasPageWidth = Math.max(400, imgWidth + actualPadding * 2);
const canvasPageHeight = Math.max(600, imgHeight + actualPadding * 2);
const mainCanvas = document.createElement('canvas');
mainCanvas.width = canvasPageWidth;
mainCanvas.height = canvasPageHeight;
const ctx = mainCanvas.getContext('2d');
// 1. Draw Page Background (Parchment, Stains)
drawParchmentTexture(ctx, canvasPageWidth, canvasPageHeight, pageBackgroundColor, stainColor, stainIntensity, textColor);
// 2. Process and Draw the Image
const imgCanvas = document.createElement('canvas');
imgCanvas.width = imgWidth;
imgCanvas.height = imgHeight;
const imgCtx = imgCanvas.getContext('2d');
imgCtx.drawImage(originalImg, 0, 0, imgWidth, imgHeight);
if (imageFilter !== 'none') {
applyImageFilterToContext(imgCtx, imageFilter);
}
const imgDrawX = (canvasPageWidth - imgWidth) / 2;
const imgDrawY = (canvasPageHeight - imgHeight) / 2;
ctx.drawImage(imgCanvas, imgDrawX, imgDrawY);
// 3. Draw Image Border
if (imageBorderWidth > 0) {
ctx.strokeStyle = imageBorderColor;
ctx.lineWidth = imageBorderWidth;
ctx.strokeRect(imgDrawX - imageBorderWidth / 2, imgDrawY - imageBorderWidth / 2,
imgWidth + imageBorderWidth, imgHeight + imageBorderWidth);
}
// 4. Add Grimoire Text
ctx.font = `${textFontSize}px ${grimoireFont}`;
ctx.fillStyle = textColor;
ctx.textAlign = 'center';
// Calculate available width for text to wrap if needed
const textMargin = actualPadding * 0.5;
const maxTextWidth = canvasPageWidth - textMargin * 2;
// Simple text wrapping (basic)
const words = customText.split(' ');
let line = '';
let textY = canvasPageHeight - actualPadding * 0.8; // Start text near bottom margin
const lineHeight = textFontSize * 1.4;
// Collect lines to draw them centered vertically if multi-line
const lines = [];
for (let n = 0; n < words.length; n++) {
const testLine = line + words[n] + ' ';
const metrics = ctx.measureText(testLine);
const testWidth = metrics.width;
if (testWidth > maxTextWidth && n > 0) {
lines.push(line.trim());
line = words[n] + ' ';
} else {
line = testLine;
}
}
lines.push(line.trim());
textY = canvasPageHeight - (actualPadding / 2) - ((lines.length -1) * lineHeight) / 2 ;
// Adjust textY based on number of lines if starting from bottom up, or use fixed position
if (lines.length === 1) { // single line text position
textY = canvasPageHeight - actualPadding / 2;
ctx.textBaseline = 'bottom';
ctx.fillText(lines[0], canvasPageWidth / 2, textY);
} else { // multi-line text position (simple top-down from a point)
textY = canvasPageHeight - actualPadding * 0.8; // Start position for multi-line title
if(textY < imgDrawY + imgHeight + lineHeight) textY = imgDrawY + imgHeight + lineHeight*2; // Ensure below image
if(textY + (lines.length-1)*lineHeight > canvasPageHeight - textMargin/2 ) textY = canvasPageHeight - textMargin/2 - (lines.length-1)*lineHeight; //Ensure not off page
ctx.textBaseline = 'top';
for(let i = 0; i < lines.length; i++) {
ctx.fillText(lines[i], canvasPageWidth / 2, textY + (i * lineHeight));
}
}
// 5. Add Occult Symbols
const symbolSize = textFontSize * 1.2;
for (let i = 0; i < symbolCount; i++) {
let sx, sy, validPosition = false;
let attempts = 0;
while(!validPosition && attempts < 20) { // Try to find a good spot
sx = Math.random() * (canvasPageWidth - symbolSize) + symbolSize / 2;
sy = Math.random() * (canvasPageHeight - symbolSize) + symbolSize / 2;
// Check if symbol is in margins (not overlapping central image too much)
const margin = symbolSize * 0.5; // Buffer around image
const isOutsideImageX = (sx < imgDrawX - margin || sx > imgDrawX + imgWidth + margin);
const isOutsideImageY = (sy < imgDrawY - margin || sy > imgDrawY + imgHeight + margin);
if (isOutsideImageX || isOutsideImageY) {
validPosition = true;
}
// Also try to avoid text area roughly (simple check)
if (sy > textY - lineHeight && sy < textY + lines.length * lineHeight && Math.abs(sx - canvasPageWidth/2) < maxTextWidth/2) {
validPosition = false;
}
attempts++;
}
if(validPosition) { // Draw if a position is found (or fallback to anywhere if too many tries)
drawOccultSymbol(ctx, sx, sy, symbolSize, textColor);
} else { // Fallback: draw it anyway, perhaps slightly smaller or more transparent
drawOccultSymbol(ctx, sx, sy, symbolSize * 0.8, hexToRgba(textColor,0.7));
}
}
return mainCanvas;
}
Apply Changes