You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg,
titleText = "Mystic Elixir",
subtitleText = "Pure & Potent",
bodyText = "A wondrous concoction of distilled moonbeams, phoenix feathers, and whispering willow bark. Brewed under celestial alignments, this elixir is guaranteed to invigorate the spirit and illuminate the mind. Take one spoonful when the stars align for maximum potency.",
footerText = "Arcane Apothecary Guild - Est. MCDLXII",
fontName = "IM Fell English SC",
fallbackFont = "Times New Roman, serif",
backgroundColor = "#f2e8d9", // Aged paper cream
textColor = "#4a3b31", // Dark sepia/brown
borderColor = "#6f5f51", // Muted brown for border
borderStyle = "double", // "single", "double", "none"
borderWidth = 2, // px, for each line of the border
imagePlacement = "center", // "center", "background", "none"
centerImageScale = 0.7, // Relative scale for centered image (0-1 of its allocated space)
backgroundImageOpacity = 0.1, // Opacity for background image (0-1)
vignetteOpacity = 0.35, // Opacity for vignette (0-1)
textureIntensity = 0.05 // Intensity of noise texture (0-1), 0 for none
) {
// --- Helper Functions ---
async function loadFontIfNeeded(fontToLoad, baseFont) {
try {
// Check if font is already loaded or is generic
if (document.fonts.check(`12px "${fontToLoad}"`) || fontToLoad.toLowerCase() === baseFont.toLowerCase()) {
return `"${fontToLoad}", ${baseFont}`;
}
// Attempt to load from Google Fonts (common for 'IM Fell English SC')
// More robust would be specific URLs for other fonts.
// For "IM Fell English SC":
if (fontToLoad === "IM Fell English SC") {
const font = new FontFace(fontToLoad, `url(https://fonts.gstatic.com/s/imfellenglishsc/v17/a8IENpD3CDX-4zrWfr1VY879qYWc05R9d_k.woff2)`);
await font.load();
document.fonts.add(font);
return `"${fontToLoad}", ${baseFont}`;
}
console.warn(`Font "${fontToLoad}" not preloaded and no specific loader defined. Using fallback "${baseFont}".`);
return `"${baseFont}"`; // Fallback if not a known special font
} catch (e) {
console.warn(`Could not load font "${fontToLoad}", using fallback "${baseFont}". Error:`, e);
return `"${baseFont}"`;
}
}
function getWrappedLines(context, text, maxWidth, fontStyle) {
context.font = fontStyle;
const words = text.split(' ');
const lines = [];
if (!words[0]) return []; // Handle empty text
let currentLine = words[0];
for (let i = 1; i < words.length; i++) {
const word = words[i];
const testLine = currentLine + " " + word;
if (context.measureText(testLine).width < maxWidth || currentLine === "") { // Allow single word to exceed maxWidth if it's the first on line
currentLine = testLine;
} else {
lines.push(currentLine);
currentLine = word;
}
}
lines.push(currentLine);
return lines;
}
function createNoisePattern(width, height, R = 0, G = 0, B = 0, A = 20, density = 0.5) {
const noiseCanvas = document.createElement('canvas');
noiseCanvas.width = width;
noiseCanvas.height = height;
const noiseCtx = noiseCanvas.getContext('2d');
if (!noiseCtx) return null; // Canvas context might not be available in some environments
const id = noiseCtx.createImageData(width, height);
const data = id.data;
const len = data.length;
for (let i = 0; i < len; i += 4) {
if (Math.random() < density) {
data[i] = R;
data[i + 1] = G;
data[i + 2] = B;
data[i + 3] = A;
} else {
data[i + 3] = 0; // Transparent
}
}
noiseCtx.putImageData(id, 0, 0);
return noiseCanvas;
}
async function ensureImageLoaded(img) {
if (img && img.src && !img.complete) {
try {
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = () => reject(new Error("Image failed to load."));
if (img.complete) resolve(); // Already loaded
});
} catch (error) {
console.error(error.message);
return false;
}
}
return img && img.naturalWidth > 0 && img.naturalHeight > 0;
}
// --- Main Logic ---
const imageIsValidAndLoaded = await ensureImageLoaded(originalImg);
const effectiveFont = await loadFontIfNeeded(fontName, fallbackFont);
const CANVAS_W = 400;
const CANVAS_H = 600;
const PADDING = 20;
const canvas = document.createElement('canvas');
canvas.width = CANVAS_W;
canvas.height = CANVAS_H;
const ctx = canvas.getContext('2d');
if (!ctx) return document.createElement('div'); // Fallback if context fails
// 1. Background Color
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
// 2. Background Image (if specified)
if (imagePlacement === 'background' && imageIsValidAndLoaded) {
ctx.save();
ctx.globalAlpha = backgroundImageOpacity;
const imgAspect = originalImg.naturalWidth / originalImg.naturalHeight;
const canvasAspect = CANVAS_W / CANVAS_H;
let sx = 0, sy = 0, sWidth = originalImg.naturalWidth, sHeight = originalImg.naturalHeight;
if (imgAspect > canvasAspect) { // Image wider than canvas aspect (crop sides)
sWidth = originalImg.naturalHeight * canvasAspect;
sx = (originalImg.naturalWidth - sWidth) / 2;
} else { // Image taller than canvas aspect (crop top/bottom)
sHeight = originalImg.naturalWidth / canvasAspect;
sy = (originalImg.naturalHeight - sHeight) / 2;
}
ctx.drawImage(originalImg, sx, sy, sWidth, sHeight, 0, 0, CANVAS_W, CANVAS_H);
ctx.restore();
}
// 3. Texture Overlay
if (textureIntensity > 0) {
const noiseAlpha = Math.floor(Math.max(0, Math.min(1, textureIntensity)) * 50); // Map 0-1 intensity to 0-50 alpha
const noiseTile = createNoisePattern(100, 100, 0, 0, 0, noiseAlpha, 0.5);
if (noiseTile) {
const pattern = ctx.createPattern(noiseTile, 'repeat');
if (pattern) {
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
}
}
}
// --- Content Area and Borders Setup ---
const contentX = PADDING;
const contentY = PADDING;
const contentW = CANVAS_W - 2 * PADDING;
const contentH = CANVAS_H - 2 * PADDING;
// Draw Borders (within the contentX/Y/W/H area)
if (borderStyle !== "none") {
ctx.strokeStyle = borderColor;
ctx.lineWidth = borderWidth;
const bw_half = borderWidth / 2;
// Outer border line
ctx.strokeRect(contentX + bw_half, contentY + bw_half, contentW - borderWidth, contentH - borderWidth);
if (borderStyle === "double") {
const gap = 5; // Space between double border lines
const innerBorderOffset = borderWidth + gap;
ctx.strokeRect(
contentX + innerBorderOffset + bw_half,
contentY + innerBorderOffset + bw_half,
contentW - 2 * innerBorderOffset - borderWidth,
contentH - 2 * innerBorderOffset - borderWidth
);
}
}
// Calculate available text block area (inside borders and with internal margins)
let textBlockBorderOffset = 0;
if (borderStyle !== "none") {
textBlockBorderOffset = borderWidth;
if (borderStyle === "double") {
textBlockBorderOffset += 5 + borderWidth; // gap + inner border width
}
}
const textMargin = 15; // Inner margin for text from borders
const availableTextX = contentX + textBlockBorderOffset + textMargin;
const availableTextY = contentY + textBlockBorderOffset + textMargin;
const availableTextW = contentW - 2 * (textBlockBorderOffset + textMargin);
const availableTextH = contentH - 2 * (textBlockBorderOffset + textMargin);
const textCenterX = availableTextX + availableTextW / 2;
let currentY = availableTextY;
ctx.fillStyle = textColor;
ctx.textAlign = 'center';
// --- Text Elements and Centered Image ---
// Title
if (titleText) {
const titleFontSize = 36;
const titleLineHeight = titleFontSize * 1.1;
ctx.font = `bold ${titleFontSize}px ${effectiveFont}`;
const titleLines = getWrappedLines(ctx, titleText, availableTextW, `bold ${titleFontSize}px ${effectiveFont}`);
for (const line of titleLines) {
ctx.fillText(line, textCenterX, currentY + titleFontSize * 0.8); // Adjust for baseline
currentY += titleLineHeight;
}
currentY += titleLineHeight * 0.3; // Extra space after title
}
// Subtitle
if (subtitleText) {
const subtitleFontSize = 20;
const subtitleLineHeight = subtitleFontSize * 1.2;
ctx.font = `${subtitleFontSize}px ${effectiveFont}`;
const subtitleLines = getWrappedLines(ctx, subtitleText, availableTextW * 0.9, `${subtitleFontSize}px ${effectiveFont}`); // slightly less width for subtitle
for (const line of subtitleLines) {
ctx.fillText(line, textCenterX, currentY + subtitleFontSize * 0.8);
currentY += subtitleLineHeight;
}
currentY += subtitleLineHeight * 0.5; // Extra space after subtitle
}
// Optional Separator (simple line)
if (titleText || subtitleText) { // Add separator if there's any text above
currentY += 5;
ctx.beginPath();
ctx.moveTo(availableTextX + availableTextW * 0.15, currentY);
ctx.lineTo(availableTextX + availableTextW * 0.85, currentY);
ctx.lineWidth = Math.max(1, borderWidth / 2); // Thinner line relative to border
ctx.strokeStyle = textColor; // Use text color for separator consistency
ctx.stroke();
currentY += 15;
}
// Centered Image
const centeredImgMaxH = 120; // Max height for the centered image
let centeredImgActualH = 0;
if (imagePlacement === 'center' && imageIsValidAndLoaded) {
let imgDrawW = originalImg.naturalWidth;
let imgDrawH = originalImg.naturalHeight;
const imgMaxW = availableTextW * 0.8; // Max width for image, leave some padding
// Scale to fit, preserving aspect ratio
if (imgDrawW > imgMaxW) {
const ratio = imgMaxW / imgDrawW;
imgDrawW = imgMaxW;
imgDrawH *= ratio;
}
if (imgDrawH > centeredImgMaxH) {
const ratio = centeredImgMaxH / imgDrawH;
imgDrawH = centeredImgMaxH;
imgDrawW *= ratio;
}
imgDrawW *= centerImageScale; // Apply user scale
imgDrawH *= centerImageScale;
if (imgDrawW > 0 && imgDrawH > 0 && (currentY + imgDrawH) < (availableTextY + availableTextH - 50) ) { // Check if fits
const imgX = textCenterX - imgDrawW / 2;
ctx.drawImage(originalImg, imgX, currentY, imgDrawW, imgDrawH);
currentY += imgDrawH;
centeredImgActualH = imgDrawH;
currentY += 20; // Space after image
}
}
// Footer setup
const footerFontSize = 13;
const footerReserveHeight = footerFontSize * 2.5; // Includes lines and spacing
const footerBaselineY = availableTextY + availableTextH - footerFontSize * 0.5; // Baseline for last line of footer
// Body Text
if (bodyText) {
const bodyFontSize = 15;
const bodyLineHeight = bodyFontSize * 1.3;
ctx.font = `${bodyFontSize}px ${effectiveFont}`;
ctx.textAlign = 'center';
const maxBodyY = footerBaselineY - footerReserveHeight; // Where body text must end
const bodyLines = getWrappedLines(ctx, bodyText, availableTextW, `${bodyFontSize}px ${effectiveFont}`);
for (const line of bodyLines) {
if (currentY + bodyLineHeight < maxBodyY) {
ctx.fillText(line, textCenterX, currentY + bodyFontSize * 0.8);
currentY += bodyLineHeight;
} else {
// Optional: Add "..." or just stop printing
if (currentY < maxBodyY) { // Try to fit one last truncated line
let partialLine = line;
while (ctx.measureText(partialLine + "...").width > availableTextW && partialLine.length > 0) {
partialLine = partialLine.slice(0, -1);
}
ctx.fillText(partialLine + "...", textCenterX, currentY + bodyFontSize * 0.8);
}
break;
}
}
}
// Footer Text (drawn from bottom up for simple multi-line)
if (footerText) {
ctx.font = `italic ${footerFontSize}px ${effectiveFont}`;
const footerLines = getWrappedLines(ctx, footerText, availableTextW, `italic ${footerFontSize}px ${effectiveFont}`).reverse();
let footerCurrentY = footerBaselineY;
for (const line of footerLines) {
ctx.fillText(line, textCenterX, footerCurrentY);
footerCurrentY -= footerFontSize * 1.2; // Move upwards for next line
}
}
// 5. Vignette
if (vignetteOpacity > 0) {
ctx.save();
const vignetteOuterRadius = Math.max(CANVAS_W, CANVAS_H) * 0.7;
const vignetteInnerRadius = Math.min(CANVAS_W, CANVAS_H) * 0.3;
const gradient = ctx.createRadialGradient(
CANVAS_W / 2, CANVAS_H / 2, vignetteInnerRadius,
CANVAS_W / 2, CANVAS_H / 2, vignetteOuterRadius
);
gradient.addColorStop(0, 'rgba(0,0,0,0)');
gradient.addColorStop(1, `rgba(0,0,0,${vignetteOpacity})`);
ctx.fillStyle = gradient;
ctx.globalCompositeOperation = 'source-over'; // Ensure it draws normally on top
ctx.fillRect(0, 0, CANVAS_W, CANVAS_H);
ctx.restore();
}
return canvas;
}
Apply Changes