You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
pageTitle = "Arcane Formulae",
mainText = "Hic iacet draco absconditus, qui se ipsum devorat et regenerat.\n\nSolve et coagula, et invenies lapidem occultum. Materia prima ubique invenitur, sed ab stultis contemnitur.\n\nIn igne philosophico, nigredo in albedinem, et albedo in rubedinem transmutantur. Aurum nostrum non est aurum vulgi, sed spiritus vivus, qui omnia corpora penetrat et perficit.",
fontFamily = "'Dancing Script', cursive",
textColor = "#5D4037", /* Dark Brown */
paperColor = "#F0E6D2", /* Pale Parchment */
borderColor = "#8C7853", /* Darker Parchment */
imageBorderColor = "#70543E" /* Darker brown for image border */
) {
const FONT_TO_LOAD_NAME = 'Dancing Script';
const FONT_TO_LOAD_URL = 'https://fonts.gstatic.com/s/dancingscript/v25/If2cXTr6YS-zF4S-kcSWSVi_sxjsohD9F楫5EW.woff2';
let actualFontFamily = fontFamily;
if (fontFamily.toLowerCase().includes('dancing script')) {
let fontLoaded = false;
try { // Check if font is already available
if (document.fonts && typeof document.fonts.check === 'function') {
if (document.fonts.check(`12px "${FONT_TO_LOAD_NAME}"`)) {
fontLoaded = true;
}
}
} catch (e) { /* Ignore errors in checking */ }
if (!fontLoaded && typeof FontFace === 'function') {
const customFont = new FontFace(FONT_TO_LOAD_NAME, `url(${FONT_TO_LOAD_URL})`);
try {
await customFont.load();
document.fonts.add(customFont);
console.log(`${FONT_TO_LOAD_NAME} font loaded.`);
actualFontFamily = `"${FONT_TO_LOAD_NAME}", ${fontFamily.split(',').slice(1).join(',') || 'cursive'}`;
} catch (e) {
console.error(`Font ${FONT_TO_LOAD_NAME} failed to load:`, e);
actualFontFamily = fontFamily.split(',').slice(1).join(',') || 'cursive'; // Fallback
}
} else if (fontLoaded) {
actualFontFamily = `"${FONT_TO_LOAD_NAME}", ${fontFamily.split(',').slice(1).join(',') || 'cursive'}`;
} else { // FontFace API not supported or font not found
actualFontFamily = fontFamily.split(',').slice(1).join(',') || 'cursive';
}
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const PAGE_WIDTH = 800;
const PAGE_HEIGHT = 1100;
canvas.width = PAGE_WIDTH;
canvas.height = PAGE_HEIGHT;
// 1. Draw Paper Background
ctx.fillStyle = paperColor;
ctx.fillRect(0, 0, PAGE_WIDTH, PAGE_HEIGHT);
// Add subtle noise to paper
for (let i = 0; i < 30000; i++) {
const x = Math.random() * PAGE_WIDTH;
const y = Math.random() * PAGE_HEIGHT;
const grayShade = Math.random() * 50 + 200; // Light gray specks
const alpha = Math.random() * 0.08;
ctx.fillStyle = `rgba(${grayShade},${grayShade},${grayShade},${alpha})`;
ctx.fillRect(x, y, Math.random()*2+1, Math.random()*2+1);
}
// Add subtle stains (simplified)
function drawStain(cx, cy, baseR, baseG, baseB) {
const numLayers = 5 + Math.floor(Math.random() * 5);
const maxRadius = 40 + Math.random() * 60;
for (let i = 0; i < numLayers; i++) {
const radiusX = maxRadius * ( (numLayers - i) / numLayers ) * (0.7 + Math.random() * 0.6);
const radiusY = maxRadius * ( (numLayers - i) / numLayers ) * (0.7 + Math.random() * 0.6);
const alpha = (0.01 + Math.random() * 0.03) * (i / numLayers + 0.2);
const r = Math.max(0, Math.min(255, baseR + Math.floor(Math.random() * 20) - 10));
const g = Math.max(0, Math.min(255, baseG + Math.floor(Math.random() * 20) - 10));
const b = Math.max(0, Math.min(255, baseB + Math.floor(Math.random() * 20) - 10));
ctx.fillStyle = `rgba(${r},${g},${b},${alpha})`;
ctx.beginPath();
ctx.ellipse(
cx + (Math.random() - 0.5) * 20,
cy + (Math.random() - 0.5) * 20,
radiusX, radiusY,
Math.random() * Math.PI * 2, 0, Math.PI * 2
);
ctx.fill();
}
}
const stainBaseR = parseInt(borderColor.slice(1,3), 16); // Use borderColor as base for stains
const stainBaseG = parseInt(borderColor.slice(3,5), 16);
const stainBaseB = parseInt(borderColor.slice(5,7), 16);
for(let k=0; k<5; k++) { // Draw a few stains randomly
drawStain(Math.random()*PAGE_WIDTH, Math.random()*PAGE_HEIGHT, stainBaseR, stainBaseG, stainBaseB);
}
// 2. Draw Page Border
const BORDER_MARGIN = 25;
const BORDER_THICKNESS_OUTER = 8;
const BORDER_THICKNESS_INNER = 1;
ctx.strokeStyle = borderColor;
ctx.lineWidth = BORDER_THICKNESS_OUTER;
ctx.strokeRect(
BORDER_MARGIN, BORDER_MARGIN,
PAGE_WIDTH - 2 * BORDER_MARGIN, PAGE_HEIGHT - 2 * BORDER_MARGIN
);
ctx.strokeStyle = textColor;
ctx.lineWidth = BORDER_THICKNESS_INNER;
ctx.strokeRect(
BORDER_MARGIN + BORDER_THICKNESS_OUTER / 2 + 2, BORDER_MARGIN + BORDER_THICKNESS_OUTER / 2 + 2,
PAGE_WIDTH - 2 * (BORDER_MARGIN + BORDER_THICKNESS_OUTER / 2 + 2), PAGE_HEIGHT - 2 * (BORDER_MARGIN + BORDER_THICKNESS_OUTER / 2 + 2)
);
// Content margins
const CONTENT_MARGIN_X = BORDER_MARGIN + BORDER_THICKNESS_OUTER + 20;
const CONTENT_MARGIN_Y_TOP = BORDER_MARGIN + BORDER_THICKNESS_OUTER + 30;
const CONTENT_MARGIN_Y_BOTTOM = BORDER_MARGIN + BORDER_THICKNESS_OUTER + 20;
const CONTENT_WIDTH = PAGE_WIDTH - 2 * CONTENT_MARGIN_X;
// 3. Draw Title
ctx.fillStyle = textColor;
ctx.textAlign = 'center';
const titleFontSize = 52;
ctx.font = `bold ${titleFontSize}px ${actualFontFamily}`;
const titleY = CONTENT_MARGIN_Y_TOP + titleFontSize;
ctx.fillText(pageTitle, PAGE_WIDTH / 2, titleY);
let currentY = titleY + titleFontSize * 0.5; // Space after title
// Decorative line
currentY += 15;
ctx.beginPath();
ctx.moveTo(CONTENT_MARGIN_X + 50, currentY);
ctx.lineTo(PAGE_WIDTH - CONTENT_MARGIN_X - 50, currentY);
ctx.lineWidth = 0.5;
ctx.strokeStyle = textColor;
ctx.stroke();
currentY += 25;
// 4. Draw Image
const maxImgWidth = CONTENT_WIDTH * 0.8;
const maxImgHeight = PAGE_HEIGHT * 0.3;
const imgAspect = originalImg.width / originalImg.height;
let imgDrawWidth = maxImgWidth;
let imgDrawHeight = imgDrawWidth / imgAspect;
if (imgDrawHeight > maxImgHeight) {
imgDrawHeight = maxImgHeight;
imgDrawWidth = imgDrawHeight * imgAspect;
}
const imgX = (PAGE_WIDTH - imgDrawWidth) / 2;
const imgY = currentY;
ctx.save();
ctx.filter = 'sepia(0.7) contrast(1.1) brightness(0.95) saturate(0.8)';
ctx.drawImage(originalImg, imgX, imgY, imgDrawWidth, imgDrawHeight);
ctx.restore();
// Image border
ctx.strokeStyle = imageBorderColor;
ctx.lineWidth = 3;
ctx.strokeRect(imgX - 1.5, imgY - 1.5, imgDrawWidth + 3, imgDrawHeight + 3); // outer slightly thicker
ctx.strokeStyle = paperColor; // Inner "highlight" line to make it look inset
ctx.lineWidth = 1;
ctx.strokeRect(imgX + 2.5, imgY + 2.5, imgDrawWidth - 5, imgDrawHeight - 5);
currentY = imgY + imgDrawHeight + 30; // Space after image
// 5. Draw Main Text
const textFontSize = 20;
const lineHeight = textFontSize * 1.5;
const paragraphIndentSpaces = 4; // Indent for first line of each paragraph
ctx.font = `${textFontSize}px ${actualFontFamily}`;
ctx.fillStyle = textColor;
ctx.textAlign = 'left';
function wrapAndDrawText(context, text, x, y, maxWidth, lineHeightVal, indentSpaces) {
const paragraphs = text.split('\n\n'); // Split by double newline for paragraphs
let localCurrentY = y;
const indentString = ' '.repeat(indentSpaces);
for (const paragraph of paragraphs) {
if (paragraph.trim() === "") {
localCurrentY += lineHeightVal;
continue;
}
const words = paragraph.replace(/\n/g, ' ').split(' '); // Replace single newlines with space for flow
let line = indentString;
for (let n = 0; n < words.length; n++) {
const testLine = line + words[n] + ' ';
const metrics = context.measureText(testLine.trimEnd()); // Measure without trailing space
const testWidth = metrics.width;
if (testWidth > maxWidth && line !== indentString) {
context.fillText(line.trimEnd(), x, localCurrentY);
line = indentString + words[n] + ' ';
localCurrentY += lineHeightVal;
} else {
line = testLine;
}
}
context.fillText(line.trimEnd(), x, localCurrentY);
localCurrentY += lineHeightVal; // Move to next line for new paragraph
}
return localCurrentY;
}
const textBlockX = CONTENT_MARGIN_X + 20;
const textBlockWidth = CONTENT_WIDTH - 40;
currentY = wrapAndDrawText(ctx, mainText, textBlockX, currentY, textBlockWidth, lineHeight, paragraphIndentSpaces);
// 6. Draw Alchemical Symbols (optional footer decoration)
currentY += 20; // Space before symbols
if (currentY < PAGE_HEIGHT - CONTENT_MARGIN_Y_BOTTOM - 30) { // Only if space allows
const symbols = ['☉', '☽', '☿', '♀', '♂', '♃', '♄', '△', '▽']; // Sun, Moon, Mercury, Venus, Mars, Jupiter, Saturn, Triangle, Inverted Triangle
ctx.textAlign = 'center';
ctx.font = `28px ${actualFontFamily}`; // Slightly larger than text
let symbolString = "";
for(let i=0; i<5; i++) {
symbolString += symbols[Math.floor(Math.random() * symbols.length)] + " ";
}
ctx.fillText(symbolString.trim(), PAGE_WIDTH / 2, currentY);
}
return canvas;
}
Apply Changes