You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, cultName = "The Ancient Order", ritualTitle = "Sacred Rites of Passage", symbols = "๐๐คโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโ๏ธโงโงโนโฝโพโโโ", mainColor = "#3a2a1a", paperColor = "#e8d9c3", imageOpacity = 0.5) {
// Helper function: Convert hex color to RGB object
function hexToRgb(hex) {
// Ensure hex is a string and valid
if (typeof hex !== 'string' || !hex.startsWith('#') || (hex.length !== 4 && hex.length !== 7)) {
console.warn("Invalid hex color:", hex, "defaulting to black.");
return { r: 0, g: 0, b: 0 }; // Default to black if invalid
}
let shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
hex = hex.replace(shorthandRegex, function(m, r, g, b) {
return r + r + g + g + b + b;
});
const bigint = parseInt(hex.slice(1), 16);
return { r: (bigint >> 16) & 255, g: (bigint >> 8) & 255, b: bigint & 255 };
}
// Helper function: Adjust color brightness (lighten or darken)
function adjustColor(hex, amount) {
let { r, g, b } = hexToRgb(hex);
r = Math.max(0, Math.min(255, r + amount));
g = Math.max(0, Math.min(255, g + amount));
b = Math.max(0, Math.min(255, b + amount));
const toHex = c => ('0' + Math.round(c).toString(16)).slice(-2);
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
// Helper function: Wrap text to fit a max width
function wrapText(context, text, maxWidth) {
const words = text.split(' ');
const lines = [];
if (words.length === 0) return lines;
let currentLine = words[0];
for (let i = 1; i < words.length; i++) {
const word = words[i];
const width = context.measureText(currentLine + " " + word).width;
if (width < maxWidth && currentLine.length < 150) { // Added length limit for very long words
currentLine += " " + word;
} else {
lines.push(currentLine);
currentLine = word;
}
}
lines.push(currentLine);
return lines;
}
// 0. Load Font
const fontName = "IM Fell DW Pica"; // An old-style font
const fontUrl = `https://fonts.googleapis.com/css2?family=IM+Fell+DW+Pica:ital@0;1&display=swap`;
try {
if (typeof document !== 'undefined' && document.fonts && !document.fonts.check(`12px "${fontName}"`)) {
const fontLink = document.createElement('link');
fontLink.href = fontUrl;
fontLink.rel = 'stylesheet';
document.head.appendChild(fontLink);
// Wait for font to load
await document.fonts.load(`1em "${fontName}"`);
await document.fonts.load(`italic 1em "${fontName}"`);
}
} catch (e) {
console.warn(`Font "${fontName}" failed to load or font API unsupported, using fallback fonts.`, e);
}
// 1. Canvas Setup
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const docWidth = 800;
const docHeight = Math.round(docWidth * 1.414); // A-series paper aspect ratio
canvas.width = docWidth;
canvas.height = docHeight;
// 2. Background - Aged Paper
ctx.fillStyle = paperColor;
ctx.fillRect(0, 0, docWidth, docHeight);
// Texture (subtle noise)
const numTexturePixels = Math.round((docWidth * docHeight) / 25); // Density of specks
const paperRgb = hexToRgb(paperColor);
for (let i = 0; i < numTexturePixels; i++) {
const x = Math.random() * docWidth;
const y = Math.random() * docHeight;
const alpha = Math.random() * 0.08 + 0.02;
const shadeVariation = Math.random() * 40 - 20; // +/- 20 variation from base
const r = Math.max(0, Math.min(255, paperRgb.r + shadeVariation));
const g = Math.max(0, Math.min(255, paperRgb.g + shadeVariation));
const b = Math.max(0, Math.min(255, paperRgb.b + shadeVariation));
ctx.fillStyle = `rgba(${Math.round(r)},${Math.round(g)},${Math.round(b)},${alpha})`;
ctx.fillRect(x, y, Math.random() * 2 + 1, Math.random() * 2 + 1);
}
// Stains
const numStains = 6 + Math.floor(Math.random() * 6);
const stainColorBase = adjustColor(paperColor, -45); // Darker, desaturated paper color for stains
const stainRgb = hexToRgb(stainColorBase);
for (let i = 0; i < numStains; i++) {
const x = Math.random() * docWidth;
const y = Math.random() * docHeight;
const radius = (Math.random() * 60 + 40) * (docWidth / 800);
const grad = ctx.createRadialGradient(x, y, radius * 0.1, x, y, radius);
grad.addColorStop(0, `rgba(${stainRgb.r}, ${stainRgb.g}, ${stainRgb.b}, ${Math.random() * 0.15 + 0.05})`);
grad.addColorStop(0.7, `rgba(${stainRgb.r}, ${stainRgb.g}, ${stainRgb.b}, ${Math.random() * 0.05})`);
grad.addColorStop(1, `rgba(${stainRgb.r}, ${stainRgb.g}, ${stainRgb.b}, 0)`);
ctx.fillStyle = grad;
ctx.beginPath();
ctx.moveTo(x + radius * Math.cos(0), y + radius * Math.sin(0));
const points = 5 + Math.floor(Math.random()*5);
for(let j = 1; j <= points; j++) {
const angle = (j/points) * Math.PI * 2;
const irregularRadius = radius * (0.7 + Math.random()*0.6);
ctx.lineTo(x + irregularRadius * Math.cos(angle), y + irregularRadius * Math.sin(angle));
}
ctx.closePath();
ctx.fill();
}
// 3. Process and Draw originalImg
const imgCanvas = document.createElement('canvas');
const imgCtx = imgCanvas.getContext('2d');
const imgBoxWidthPercentage = 0.6;
const imgBoxHeightPercentage = 0.35;
const imgBoxX = docWidth * (1 - imgBoxWidthPercentage) / 2;
const imgBoxY = docHeight * 0.28;
const imgBoxMaxW = docWidth * imgBoxWidthPercentage;
const imgBoxMaxH = docHeight * imgBoxHeightPercentage;
let iW = originalImg.width;
let iH = originalImg.height;
if (iW === 0 || iH === 0) { iW = 200; iH = 200; } // Default for potentially unloaded image
const scaleFactor = Math.min(imgBoxMaxW / iW, imgBoxMaxH / iH, 1.5); // Allow slight upscale for tiny images
const imgDrawWidth = Math.max(1, iW * scaleFactor); // Ensure min 1px
const imgDrawHeight = Math.max(1, iH * scaleFactor);
imgCanvas.width = imgDrawWidth;
imgCanvas.height = imgDrawHeight;
imgCtx.drawImage(originalImg, 0, 0, imgDrawWidth, imgDrawHeight);
const imageData = imgCtx.getImageData(0, 0, imgDrawWidth, imgDrawHeight);
const data = imageData.data;
const mainRgbForTint = hexToRgb(mainColor);
for (let j = 0; j < data.length; j += 4) {
const r = data[j];
const g = data[j + 1];
const b = data[j + 2];
const gray = 0.299 * r + 0.587 * g + 0.114 * b; // Luminosity
// Tint towards mainColor and desaturate/dim
let newR = gray * (mainRgbForTint.r / 128 * 0.4 + 0.6);
let newG = gray * (mainRgbForTint.g / 128 * 0.4 + 0.6);
let newB = gray * (mainRgbForTint.b / 128 * 0.4 + 0.6);
const dimFactor = 0.85;
data[j] = Math.min(255, newR) * dimFactor;
data[j + 1] = Math.min(255, newG) * dimFactor;
data[j + 2] = Math.min(255, newB) * dimFactor;
}
imgCtx.putImageData(imageData, 0, 0);
ctx.globalAlpha = Math.max(0, Math.min(1, imageOpacity)); // Clamp opacity
ctx.globalCompositeOperation = 'multiply';
const finalImgX = imgBoxX + (imgBoxMaxW - imgDrawWidth) / 2;
const finalImgY = imgBoxY + (imgBoxMaxH - imgDrawHeight) / 2;
if (imgDrawWidth > 0 && imgDrawHeight > 0) {
ctx.drawImage(imgCanvas, finalImgX, finalImgY);
}
ctx.globalAlpha = 1.0;
ctx.globalCompositeOperation = 'source-over';
// 4. Add Text & Symbols
ctx.fillStyle = mainColor;
const baseFontSize = docWidth / 32;
const mainFont = `"${fontName}", "Times New Roman", serif`;
// Cult Name
ctx.font = `italic ${Math.round(baseFontSize * 1.3)}px ${mainFont}`;
ctx.textAlign = 'center';
let currentTextY = docHeight * 0.12;
ctx.fillText(cultName, docWidth / 2, currentTextY);
// Ritual Title
ctx.font = `${Math.round(baseFontSize * 1.0)}px ${mainFont}`;
currentTextY += baseFontSize * 2.0;
ctx.fillText(ritualTitle, docWidth / 2, currentTextY);
const ritualPhrases = [
"Et lux in tenebris non lucet.", "Vocamus te, spiritus antiqui.", "Per sanguinem et umbram.",
"Sigillum fractum est.", "Verba potentiae obscurae.", "Nox profunda, silentium sacrum.",
"Infernalis potentia, emerge!", "Circulus protectionis ducitur.", "Sacrificium acceptum sit.",
"Porta aperta est inter mundos.", "Clavis ad abyssum.", "In nomine innominabilis."
];
ctx.font = `${Math.round(baseFontSize * 0.78)}px ${mainFont}`;
ctx.textAlign = 'left';
const textBlockWidth = docWidth * 0.75;
const textBlockX = (docWidth - textBlockWidth) / 2;
let paragraphY = Math.max(finalImgY + imgDrawHeight + baseFontSize * 2.5, currentTextY + baseFontSize * 3.5);
const numParagraphs = 2 + Math.floor(Math.random() * 2);
for (let i = 0; i < numParagraphs; i++) {
if (paragraphY > docHeight - baseFontSize * 5) break;
let paragraphText = "";
const numSentences = 2 + Math.floor(Math.random() * 3);
for(let s=0; s<numSentences; s++) {
paragraphText += ritualPhrases[Math.floor(Math.random() * ritualPhrases.length)] + " ";
}
const lines = wrapText(ctx, paragraphText.trim(), textBlockWidth);
lines.forEach(line => {
if (paragraphY < docHeight - baseFontSize * 2.5) {
ctx.save();
const jitterX = (Math.random() * 2 - 1) * (baseFontSize * 0.06);
const jitterY = (Math.random() * 2 - 1) * (baseFontSize * 0.06);
ctx.globalAlpha = 0.65 + Math.random() * 0.3;
ctx.fillText(line, textBlockX + jitterX, paragraphY + jitterY);
ctx.restore();
paragraphY += baseFontSize * 1.15; // Line height
}
});
paragraphY += baseFontSize * 0.8;
}
// Scatter Symbols
ctx.font = `${Math.round(baseFontSize * 1.9)}px "Segoe UI Symbol", "Symbola", "Noto Sans Symbols", sans-serif`;
const symbolChars = symbols.split('');
const numDecorativeSymbols = 15 + Math.floor(Math.random() * 10);
for (let i = 0; i < numDecorativeSymbols && symbolChars.length > 0; i++) {
const sym = symbolChars[Math.floor(Math.random() * symbolChars.length)];
let sx = Math.random() * docWidth;
let sy = Math.random() * docHeight;
const margin = docWidth * 0.05;
if (Math.random() < 0.65) {
if (Math.random() < 0.5) { // Left/Right margins
sx = Math.random() * margin + (Math.random() < 0.5 ? 0 : docWidth - margin - ctx.measureText(sym).width);
} else { // Top/Bottom margins
sy = Math.random() * margin + (Math.random() < 0.5 ? baseFontSize*1.9 : docHeight - margin);
}
}
ctx.save();
ctx.fillStyle = mainColor; // Ensure symbols use mainColor
ctx.globalAlpha = 0.1 + Math.random() * 0.25; // Faint symbols
ctx.translate(sx, sy);
ctx.rotate((Math.random() - 0.5) * 0.7);
ctx.fillText(sym, 0, 0);
ctx.restore();
}
// 5. Final Aging Touches
const vignetteColorRgb = hexToRgb(adjustColor(mainColor, -30));
// Vignette
const outerRadius = docWidth * 0.8;
const innerRadius = docWidth * 0.25;
const vignetteGrad = ctx.createRadialGradient(docWidth / 2, docHeight / 2, innerRadius, docWidth / 2, docHeight / 2, outerRadius);
vignetteGrad.addColorStop(0, `rgba(${vignetteColorRgb.r},${vignetteColorRgb.g},${vignetteColorRgb.b},0)`);
vignetteGrad.addColorStop(1, `rgba(${vignetteColorRgb.r},${vignetteColorRgb.g},${vignetteColorRgb.b},0.35)`);
ctx.fillStyle = vignetteGrad;
ctx.fillRect(0, 0, docWidth, docHeight);
// Subtle "burnt" or worn edges
ctx.save();
ctx.globalCompositeOperation = 'multiply';
const edgeDarkness = 0.2;
const edgeWidth = Math.round(40 * (docWidth/800));
const edgeColor = `rgba(${vignetteColorRgb.r},${vignetteColorRgb.g},${vignetteColorRgb.b},${edgeDarkness})`;
const transparentEdge = `rgba(${vignetteColorRgb.r},${vignetteColorRgb.g},${vignetteColorRgb.b},0)`;
let gradEdge = ctx.createLinearGradient(0,0,0,edgeWidth);
gradEdge.addColorStop(0, edgeColor); gradEdge.addColorStop(1, transparentEdge);
ctx.fillStyle = gradEdge; ctx.fillRect(0,0,docWidth,edgeWidth);
gradEdge = ctx.createLinearGradient(0,docHeight-edgeWidth,0,docHeight);
gradEdge.addColorStop(0, transparentEdge); gradEdge.addColorStop(1, edgeColor);
ctx.fillStyle = gradEdge; ctx.fillRect(0,docHeight-edgeWidth,docWidth,edgeWidth);
gradEdge = ctx.createLinearGradient(0,0,edgeWidth,0);
gradEdge.addColorStop(0, edgeColor); gradEdge.addColorStop(1, transparentEdge);
ctx.fillStyle = gradEdge; ctx.fillRect(0,0,edgeWidth,docHeight);
gradEdge = ctx.createLinearGradient(docWidth-edgeWidth,0,docWidth,0);
gradEdge.addColorStop(0, transparentEdge); gradEdge.addColorStop(1, edgeColor);
ctx.fillStyle = gradEdge; ctx.fillRect(docWidth-edgeWidth,0,edgeWidth,docHeight);
ctx.restore();
return canvas;
}
Apply Changes