You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
pageTitle = "CODEX OBSCURA",
mainText = "Fragmenta vetusta, in tenebris susurrata.\nScientia prohibita hic iacet.",
titleFontSize = 36,
bodyFontSize = 20,
pageFontFamily = "'UnifrakturMaguntia', cursive",
textColor = "#3A2F2F",
imageEffect = "sepia", // "none", "grayscale", "sepia"
symbols = "✧✦★✡☥☯♈♉♊♋♌♍♎♏♐♑♒♓☉☽☿♀⊕♂♃♄⊗⊙∴∵∆∇ΣΩ",
symbolFontFamily = "Arial", // Symbols often render better with a generic font
symbolColor = "#5a0000",
symbolAlpha = 0.2,
symbolDensity = 0.05, // Proportion of "cells" (symbolSize x symbolSize) to fill
symbolSize = 22,
backgroundColor = "#EAE0C8",
pagePadding = 40
) {
// Helper to load a Google Font (specifically for UnifrakturMaguntia in this case)
const _loadGoogleFont = async (fontFamilyName, fontUrl) => {
// Check if font is already loaded or available natively
// fontFamilyName here is the pure name like "UnifrakturMaguntia"
if (document.fonts.check(`12px "${fontFamilyName}"`)) {
return true;
}
const linkId = `google-font-${fontFamilyName.replace(/\s+/g, '-')}`;
if (!document.getElementById(linkId)) {
const link = document.createElement('link');
link.id = linkId;
link.href = fontUrl;
link.rel = 'stylesheet';
document.head.appendChild(link);
// Wait for the stylesheet to load
await new Promise((resolve, reject) => {
let timeoutId = setTimeout(() => reject(new Error('Font stylesheet loading timeout')), 5000);
link.onload = () => { clearTimeout(timeoutId); resolve(); };
link.onerror = () => { clearTimeout(timeoutId); reject(new Error('Font stylesheet failed to load'));};
}).catch(e => console.warn(`Stylesheet for ${fontFamilyName}: ${e.message}. Attempting font load anyway.`));
}
try {
// Attempt to load the specific font weight/style
await document.fonts.load(`12px "${fontFamilyName}"`);
return true;
} catch (e) {
console.error(`Failed to load font "${fontFamilyName}" via document.fonts.load:`, e);
return false;
}
};
let actualPageFontFamily = pageFontFamily;
if (pageFontFamily.toLowerCase().includes('unifrakturmaguntia')) {
const googleFontName = 'UnifrakturMaguntia'; // The actual font family name from Google's CSS
const fontLoaded = await _loadGoogleFont(googleFontName, 'https://fonts.googleapis.com/css2?family=UnifrakturMaguntia&display=swap');
if (!fontLoaded) {
actualPageFontFamily = 'serif'; // Fallback if UnifrakturMaguntia fails
} else {
actualPageFontFamily = `'${googleFontName}', cursive`; // Use the successfully loaded font
}
}
// Helper to apply image effects
const _applyImageEffect = (img, effect) => {
const tempCanvas = document.createElement('canvas');
const natWidth = img.naturalWidth || img.width;
const natHeight = img.naturalHeight || img.height;
tempCanvas.width = natWidth;
tempCanvas.height = natHeight;
const tempCtx = tempCanvas.getContext('2d');
if (!natWidth || !natHeight) return tempCanvas; // Return empty if image invalid
tempCtx.drawImage(img, 0, 0, natWidth, natHeight);
if (effect === "none") return tempCanvas;
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const r = data[i], g = data[i+1], b = data[i+2];
if (effect === "grayscale") {
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
data[i] = data[i+1] = data[i+2] = gray;
} else if (effect === "sepia") {
data[i] = Math.min(255, 0.393*r + 0.769*g + 0.189*b);
data[i+1] = Math.min(255, 0.349*r + 0.686*g + 0.168*b);
data[i+2] = Math.min(255, 0.272*r + 0.534*g + 0.131*b);
}
}
tempCtx.putImageData(imageData, 0, 0);
return tempCanvas;
};
// Calculate dimensions
const imgNatWidth = originalImg.naturalWidth || originalImg.width;
const imgNatHeight = originalImg.naturalHeight || originalImg.height;
const titleLineHeight = titleFontSize * 1.2; // Estimated height for a line of title text
const bodyLineHeight = bodyFontSize * 1.5; // Estimated height for a line of body text
const mainTextLines = mainText.split('\n');
const canvas = document.createElement('canvas');
canvas.width = Math.max(300, imgNatWidth + 2 * pagePadding);
canvas.height = pagePadding // Top padding
+ titleLineHeight // Title block height
+ pagePadding // Space between title and image
+ imgNatHeight // Image height
+ pagePadding // Space between image and main text
+ (mainTextLines.length * bodyLineHeight) // Main text block height
+ pagePadding; // Bottom padding
const ctx = canvas.getContext('2d');
// 1. Background + Parchment Texture
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.save();
const numSpecks = Math.floor(canvas.width * canvas.height * 0.0015);
for (let i = 0; i < numSpecks; i++) {
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
const speckWidth = Math.random() * 2.5 + 0.5;
const speckHeight = Math.random() * 2.5 + 0.5;
const alpha = Math.random() * 0.25 + 0.05;
const grayVal = Math.floor(Math.random() * 100) + 30; // Darkish gray specks (30-129)
ctx.fillStyle = `rgba(${grayVal}, ${grayVal}, ${grayVal}, ${alpha})`;
ctx.fillRect(x, y, speckWidth, speckHeight);
}
ctx.restore();
// Set text baseline to top for easier Y coordinate management
ctx.textBaseline = 'top';
// 2. Processed Image (draw after background texture, before text and symbols that overlay)
const processedImgCanvas = _applyImageEffect(originalImg, imageEffect);
const imgX = (canvas.width - imgNatWidth) / 2; // Center image horizontally
const imgActualY = pagePadding + titleLineHeight + pagePadding; // Y position of image
if (imgNatWidth > 0 && imgNatHeight > 0) {
ctx.drawImage(processedImgCanvas, imgX, imgActualY, imgNatWidth, imgNatHeight);
}
// 3. Text (Title, Body)
ctx.fillStyle = textColor;
ctx.textAlign = 'center';
// Title
ctx.font = `${titleFontSize}px ${actualPageFontFamily}`;
const titleActualY = pagePadding;
ctx.fillText(pageTitle, canvas.width / 2, titleActualY);
// Body Text
ctx.font = `${bodyFontSize}px ${actualPageFontFamily}`;
let currentTextY = imgActualY + (imgNatHeight > 0 ? imgNatHeight : -pagePadding) + pagePadding; // Start Y for body text
mainTextLines.forEach(line => {
ctx.fillText(line, canvas.width / 2, currentTextY);
currentTextY += bodyLineHeight;
});
// 4. Symbols (drawn on top of everything with alpha)
if (symbols && symbols.length > 0 && symbolDensity > 0) {
ctx.save();
ctx.font = `${symbolSize}px ${symbolFontFamily}`;
ctx.fillStyle = symbolColor;
ctx.globalAlpha = symbolAlpha;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle'; // Center symbols vertically as well
const symbolChars = symbols.split(''); // Use this instead of Array.from for wider compatibility (though split handles unicode fine)
const numSymbolsToDraw = Math.floor((canvas.width / symbolSize) * (canvas.height / symbolSize) * symbolDensity);
for (let i = 0; i < numSymbolsToDraw; i++) {
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
const randomSymbol = symbolChars[Math.floor(Math.random() * symbolChars.length)];
ctx.fillText(randomSymbol, x, y);
}
ctx.restore();
}
return canvas;
}
Apply Changes