You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
bgColor = "#FDF5E6", // Page background color
borderColor = "#8B4513", // Page border color
borderWidth = 20, // Page border width
textColor = "#4A3B31", // Main text color
imageFilter = "sepia(80%) brightness(0.95)", // CSS filter for the image
imageFrame = "#5C4033", // Image frame color
imageFrameWidth = 5, // Image frame width
title = "Codex Arcanum", // Title text
body = "Incipit tractatus de lapide philosophorum. Materia prima, ex qua omnia constant, per artem nostram secreta revelat. Ignis et aqua, spiritus et corpus, in harmonia perfecta coniungantur. Cave lector, nam semita cognitionis ardua est et periclis plena.", // Body text
fontName = "IM Fell English SC", // Font family name (requires fontUrl)
fontUrl = "https://fonts.gstatic.com/s/imfellenglishsc/v17/a8IENpD3CDX-4_QHmkucFh4XcRUIxlBFncih.ttf", // URL for the font file
titleSize = 30, // Font size for title
bodySize = 15, // Font size for body text
symbolColor = "#600000", // Dark red for symbols. #6A0DAD (purple) is also nice.
symbolSize = 25, // Size of alchemical symbols
splotchColor = "#A0522D", // Color of splotches/stains (Sienna)
numSplotches = 8, // Number of splotches
numSymbols = 7 // Number of decorative symbols
) {
// Constants for canvas and layout
const canvasWidth = 800;
const canvasHeight = 1100;
const bodyLineHeight = bodySize * 1.4;
const contentPadding = borderWidth + 30; // Padding from canvas edge to content area
const innerContentWidth = canvasWidth - 2 * contentPadding;
// Create canvas
const canvas = document.createElement('canvas');
canvas.width = canvasWidth;
canvas.height = canvasHeight;
const ctx = canvas.getContext('2d');
// --- Helper Functions (defined inside to be self-contained) ---
async function loadWebFont(name, url) {
if (!name || !url) return false;
try {
const font = new FontFace(name, `url(${url})`);
await font.load();
document.fonts.add(font);
return true;
} catch (e) {
console.warn(`Failed to load font "${name}" from "${url}":`, e);
return false;
}
}
function drawAgedPaper(context, width, height, pageBgColor) {
context.fillStyle = pageBgColor;
context.fillRect(0, 0, width, height);
const numParticles = Math.floor(width * height / 25); // Adjust density
for (let i = 0; i < numParticles; i++) {
const x = Math.random() * width;
const y = Math.random() * height;
const alpha = Math.random() * 0.12 + 0.03; // Slightly more visible particles
const shade = (Math.random() - 0.5) * 40; // +/- 20 shade variation
let r = parseInt(pageBgColor.substring(1, 3), 16);
let g = parseInt(pageBgColor.substring(3, 5), 16);
let b = parseInt(pageBgColor.substring(5, 7), 16);
r = Math.min(255, Math.max(0, r + shade));
g = Math.min(255, Math.max(0, g + shade));
b = Math.min(255, Math.max(0, b + shade));
context.fillStyle = `rgba(${r}, ${g}, ${b}, ${alpha})`;
context.fillRect(x, y, Math.random() * 2 + 1, Math.random() * 2 + 1);
}
}
function drawSplotch(context, cx, cy, maxR, baseSplotchColor) {
const numCircles = Math.floor(Math.random() * 5) + 4; // 4 to 8 circles per splotch
for (let i = 0; i < numCircles; i++) {
const radius = Math.random() * maxR * 0.7 + maxR * 0.3; // Ensure splotch parts have substance
const offsetX = (Math.random() - 0.5) * radius * 0.9; // Allow circles to offset
const offsetY = (Math.random() - 0.5) * radius * 0.9;
const alpha = Math.random() * 0.18 + 0.05; // Splotches can be a bit more prominent
let r = parseInt(baseSplotchColor.substring(1, 3), 16);
let g = parseInt(baseSplotchColor.substring(3, 5), 16);
let b = parseInt(baseSplotchColor.substring(5, 7), 16);
const variation = (Math.random() - 0.5) * 60; // Color variation within splotch
r = Math.max(0, Math.min(255, r + variation));
g = Math.max(0, Math.min(255, g + variation * 0.8)); // Less green variation for browns
b = Math.max(0, Math.min(255, b + variation * 0.5)); // Even less blue variation
context.fillStyle = `rgba(${r},${g},${b}, ${alpha})`;
context.beginPath();
context.arc(cx + offsetX, cy + offsetY, radius, 0, 2 * Math.PI);
context.fill();
}
}
function wrapText(context, textToWrap, x, y, maxWidth, lineHeight, fontStyle, txtColor) {
context.font = fontStyle;
context.fillStyle = txtColor;
const words = textToWrap.split(' ');
let line = '';
let currentYPos = y;
for (let n = 0; n < words.length; n++) {
const testLine = line + words[n] + ' ';
const metrics = context.measureText(testLine);
const testWidth = metrics.width;
if (testWidth > maxWidth && n > 0) {
context.fillText(line.trim(), x, currentYPos);
line = words[n] + ' ';
currentYPos += lineHeight;
} else {
line = testLine;
}
}
context.fillText(line.trim(), x, currentYPos);
return currentYPos + lineHeight; // Return Y position after the last line of text
}
function drawAlchemicalSymbol(context, type, x, y, s, symColor) {
context.save();
context.fillStyle = symColor;
context.strokeStyle = symColor; // For symbols that might use stroke
context.lineWidth = Math.max(1, s / 12); // Adjusted line width for fill/stroke balance
context.beginPath();
switch (type) {
case 'circleDot': // Sun / Gold
context.arc(x, y, s / 2, 0, 2 * Math.PI); // Outer circle
context.stroke(); // Stroke the circle
context.beginPath(); // Start new path for dot
context.arc(x, y, s / 5.5, 0, 2 * Math.PI); // Center dot
context.fill(); // Fill the dot
break;
case 'crescent': // Moon / Silver
context.arc(x, y, s / 2, -Math.PI / 2, Math.PI / 2, false); // Outer part of crescent
// Inner cut: using arc with offset center
context.arc(x + s / 7, y, s / 2.2, Math.PI / 2 - 0.35, -Math.PI / 2 + 0.35, true);
context.closePath();
context.fill();
break;
case 'triangleUp': // Fire / Sulfur
context.moveTo(x, y - s / 2.1); // Point slightly adjusted
context.lineTo(x + s * 0.45, y + s / 4.2); // Base points adjusted
context.lineTo(x - s * 0.45, y + s / 4.2);
context.closePath();
context.fill();
break;
case 'triangleDown': // Water / Mercury
context.moveTo(x, y + s / 2.1);
context.lineTo(x + s * 0.45, y - s / 4.2);
context.lineTo(x - s * 0.45, y - s / 4.2);
context.closePath();
context.fill();
break;
case 'cross': // Simple Cross (e.g., for Antimony)
const armLength = s * 0.85; // Slightly longer arms
const armWidth = s * 0.22; // Slightly thicker arms
context.fillRect(x - armLength / 2, y - armWidth / 2, armLength, armWidth); // Horizontal
context.fillRect(x - armWidth / 2, y - armLength / 2, armWidth, armLength); // Vertical
break;
case 'square': // Salt / Earth (variant representation)
context.fillRect(x - s / 2.6, y - s / 2.6, s * 2 / 2.6, s * 2 / 2.6); // A solid square
break;
}
context.restore();
}
// --- Main Drawing Logic ---
// 0. Load Font
const fontLoaded = await loadWebFont(fontName, fontUrl);
const defaultSerif = "Times New Roman, Times, serif";
const effectiveFontFamily = fontLoaded ? `"${fontName}", ${defaultSerif}` : defaultSerif;
// 1. Background Paper
drawAgedPaper(ctx, canvasWidth, canvasHeight, bgColor);
// 2. Splotches
for (let i = 0; i < numSplotches; i++) {
const splotchX = Math.random() * canvasWidth;
const splotchY = Math.random() * canvasHeight;
const splotchRadius = (Math.random() * 0.5 + 0.5) * Math.min(canvasWidth, canvasHeight) * 0.07;
drawSplotch(ctx, splotchX, splotchY, splotchRadius, splotchColor);
}
// 3. Page Border
if (borderWidth > 0) {
ctx.strokeStyle = borderColor;
ctx.lineWidth = borderWidth;
// Draw rect from center of line to get full width inside canvas
ctx.strokeRect(borderWidth / 2, borderWidth / 2, canvasWidth - borderWidth, canvasHeight - borderWidth);
}
let currentY = contentPadding;
// 4. Title
if (title) {
ctx.fillStyle = textColor;
ctx.font = `italic ${titleSize}px ${effectiveFontFamily}`;
ctx.textAlign = "center";
ctx.fillText(title, canvasWidth / 2, currentY + titleSize * 0.8); // Adjusted baseline for better look
currentY += titleSize * 1.2 + 20;
}
// 5. Image
const maxImgHeight = canvasHeight / 2.8; // Max height for the image, adjusted
const scaledImg = {
width: originalImg.naturalWidth || originalImg.width, // Ensure dimensons are available
height: originalImg.naturalHeight || originalImg.height
};
// Scale image to fit content width and max height
if (scaledImg.width > innerContentWidth) {
const ratio = innerContentWidth / scaledImg.width;
scaledImg.width = innerContentWidth;
scaledImg.height *= ratio;
}
if (scaledImg.height > maxImgHeight) {
const ratio = maxImgHeight / scaledImg.height;
scaledImg.height = maxImgHeight;
scaledImg.width *= ratio;
}
const imgX = (canvasWidth - scaledImg.width) / 2;
const imgY = currentY;
if (imageFrameWidth > 0) {
ctx.fillStyle = imageFrame;
ctx.fillRect(
imgX - imageFrameWidth,
imgY - imageFrameWidth,
scaledImg.width + 2 * imageFrameWidth,
scaledImg.height + 2 * imageFrameWidth
);
}
ctx.filter = imageFilter || 'none';
try {
ctx.drawImage(originalImg, imgX, imgY, scaledImg.width, scaledImg.height);
} catch (e) {
console.error("Error drawing original image:", e);
ctx.filter = 'none'; // Reset filter if error occurs before
ctx.fillStyle = "#DDD"; // Light grey placeholder
ctx.fillRect(imgX, imgY, scaledImg.width, scaledImg.height);
ctx.fillStyle = textColor; // Use main text color for error message
ctx.textAlign = "center";
ctx.font = `${bodySize*0.9}px ${effectiveFontFamily}`;
wrapText(ctx, "Error displaying image.", imgX + scaledImg.width/2, imgY + scaledImg.height/2 - bodySize, scaledImg.width * 0.9, bodySize, ctx.font, textColor);
}
ctx.filter = 'none'; // Reset filter after drawing
currentY += scaledImg.height + imageFrameWidth + 25;
// 6. Body Text
let currentYAfterText = currentY;
if (body) {
ctx.textAlign = "left"; // Default for body text
currentYAfterText = wrapText(ctx, body, contentPadding, currentY, innerContentWidth, bodyLineHeight, `${bodySize}px ${effectiveFontFamily}`, textColor);
}
// 7. Decorative Symbols
const symbolTypes = ['circleDot', 'crescent', 'triangleUp', 'triangleDown', 'cross', 'square'];
const numCornerSymbols = Math.min(numSymbols, 4); // Max 4 symbols in corners
// Distance from actual canvas edge, for symbols within border or near it.
const cornerOffset = borderWidth + symbolSize / 1.5;
for (let i = 0; i < numSymbols; i++) {
const symType = symbolTypes[i % symbolTypes.length];
let sx, sy;
if (i < numCornerSymbols) { // Place first few symbols in corners gracefully
if (i === 0) { sx = cornerOffset; sy = cornerOffset; } // Top-Left
else if (i === 1) { sx = canvasWidth - cornerOffset; sy = cornerOffset; } // Top-Right
else if (i === 2) { sx = cornerOffset; sy = canvasHeight - cornerOffset; } // Bottom-Left
else { sx = canvasWidth - cornerOffset; sy = canvasHeight - cornerOffset; } // Bottom-Right
} else { // Place remaining symbols randomly in available margin spaces
const marginChoice = Math.random();
const availableTopMargin = contentPadding - borderWidth - symbolSize - 10;
const availableBottomMargin = canvasHeight - currentYAfterText - borderWidth - symbolSize - 10;
const availableSideMargin = contentPadding - borderWidth - symbolSize - 10;
if (marginChoice < 0.33 && availableTopMargin > 0) { // Top margin area
sx = contentPadding + Math.random() * innerContentWidth;
sy = borderWidth + Math.random() * availableTopMargin + symbolSize / 2;
} else if (marginChoice < 0.66 && availableBottomMargin > 0) { // Bottom margin area
sx = contentPadding + Math.random() * innerContentWidth;
sy = currentYAfterText + Math.random() * availableBottomMargin + symbolSize / 2;
} else if (availableSideMargin > 0) { // Side margins
sx = (Math.random() < 0.5) ?
borderWidth + Math.random() * availableSideMargin + symbolSize / 2 :
canvasWidth - contentPadding + Math.random() * availableSideMargin + symbolSize / 2;
sy = contentPadding + Math.random() * (currentYAfterText - contentPadding - symbolSize); // Place along text height
if (sy < contentPadding) sy = contentPadding + symbolSize; // ensure not over title
} else { // Fallback if no margin space (e.g. very dense page) - random on border line
sx = Math.random() > 0.5 ? borderWidth/2 : canvasWidth - borderWidth/2;
sy = Math.random() * canvasHeight;
}
}
// Ensure symbols are mostly within canvas and visible
sx = Math.max(symbolSize / 2 + 5, Math.min(canvasWidth - symbolSize / 2 - 5, sx));
sy = Math.max(symbolSize / 2 + 5, Math.min(canvasHeight - symbolSize / 2 - 5, sy));
drawAlchemicalSymbol(ctx, symType, sx, sy, symbolSize, symbolColor);
}
return canvas;
}
Apply Changes