You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
title = "The Abyssal Crypt",
author = "Evelyn Blackwood",
tagline = "Some secrets are buried for a reason...",
primaryColor = "crimson", // Color for the main title (e.g., "crimson", "#FF0000")
secondaryColor = "#D3D3D3", // Color for author and tagline (e.g., "lightgray", "#D3D3D3")
fontName = '"Nosifer", cursive' // Full CSS font-family string for titles/author
) {
// 1. Determine the primary font to load from Google Fonts (if applicable)
// Assumes the first font in the fontName string is the one to fetch from Google Fonts.
const primaryFontForLoading = fontName.split(',')[0].trim().replace(/^"|"$/g, '');
// 2. Dynamically load the primary font if specified and not already available
if (primaryFontForLoading) {
const fontId = `google-font-${primaryFontForLoading.replace(/[^a-z0-9]/gi, '-')}`; // Sanitize ID
if (!document.getElementById(fontId)) {
const link = document.createElement('link');
link.id = fontId;
link.rel = 'stylesheet';
link.href = `https://fonts.googleapis.com/css2?family=${primaryFontForLoading.replace(/\s/g, '+')}:wght@400&display=swap`;
document.head.appendChild(link);
try {
if (document.fonts) { // Check if FontFaceSet API is supported
await document.fonts.load(`1em "${primaryFontForLoading}"`);
} else {
// Fallback for older browsers: give some time for CSS to load the font.
// This is not foolproof but better than nothing.
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1 second
}
} catch (err) {
console.warn(`Font "${primaryFontForLoading}" could not be loaded or loading failed: ${err}. System fallbacks will be used.`);
}
} else if (document.fonts && !document.fonts.check(`1em "${primaryFontForLoading}"`)) {
// Font link tag exists, but font not yet usable, try to await its load
try {
await document.fonts.load(`1em "${primaryFontForLoading}"`);
} catch (err) {
console.warn(`Font "${primaryFontForLoading}" (existing link) failed to load: ${err}.`);
}
}
}
// 3. Create canvas and context
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Standard book cover aspect ratio (e.g., 6x9 inches) -> 600x900 pixels
const coverWidth = 600;
const coverHeight = 900;
canvas.width = coverWidth;
canvas.height = coverHeight;
// 4. Draw and process the background image
ctx.clearRect(0, 0, coverWidth, coverHeight);
// Calculate aspect ratios and drawing parameters to make the image cover the canvas
const imgAspectRatio = originalImg.width / originalImg.height;
const canvasAspectRatio = coverWidth / coverHeight;
let drawWidth, drawHeight,offsetX, offsetY;
if (originalImg.width === 0 || originalImg.height === 0) { // Handle invalid image gracefully
ctx.fillStyle = '#101020'; // Dark fallback background
ctx.fillRect(0,0,coverWidth, coverHeight);
drawWidth=0; drawHeight=0; offsetX=0; offsetY=0; // skip image drawing
} else if (imgAspectRatio > canvasAspectRatio) {
drawHeight = coverHeight;
drawWidth = drawHeight * imgAspectRatio;
offsetX = (coverWidth - drawWidth) / 2;
offsetY = 0;
ctx.drawImage(originalImg, offsetX, offsetY, drawWidth, drawHeight);
} else {
drawWidth = coverWidth;
drawHeight = drawWidth / imgAspectRatio;
offsetX = 0;
offsetY = (coverHeight - drawHeight) / 2;
ctx.drawImage(originalImg, offsetX, offsetY, drawWidth, drawHeight);
}
// Apply a dark, moody overlay to the image
ctx.fillStyle = "rgba(10, 0, 20, 0.65)"; // Dark purple/indigo overlay
ctx.fillRect(0, 0, coverWidth, coverHeight);
// Add a vignette effect (darkened edges)
const vignetteOuterRadius = Math.sqrt(Math.pow(coverWidth / 2, 2) + Math.pow(coverHeight / 2, 2));
const gradient = ctx.createRadialGradient(
coverWidth / 2, coverHeight / 2, coverWidth / 3.5, // Inner circle (lighter area)
coverWidth / 2, coverHeight / 2, vignetteOuterRadius * 0.90 // Outer circle (darker area)
);
gradient.addColorStop(0, 'rgba(0,0,0,0)');
gradient.addColorStop(1, 'rgba(0,0,0,0.85)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, coverWidth, coverHeight);
// 5. Text rendering helper function
const getLinesForText = (text, maxWidth, fontStyle) => {
// Temporarily set font to measure
const currentFont = ctx.font;
ctx.font = fontStyle;
const words = text.split(' ');
let line = '';
const lines = [];
for (let n = 0; n < words.length; n++) {
const testLine = line + words[n] + ' ';
if (ctx.measureText(testLine).width > maxWidth && n > 0) {
lines.push(line.trim());
line = words[n] + ' ';
} else {
line = testLine;
}
}
lines.push(line.trim());
ctx.font = currentFont; // Restore font
return lines;
};
const drawTextLines = (lines, x, startY, lineHeight, fontStyle, color, shadow, align = 'center', baseline = 'top') => {
ctx.font = fontStyle;
ctx.fillStyle = color;
ctx.textAlign = align;
ctx.textBaseline = baseline;
if (shadow) {
ctx.shadowColor = 'rgba(0,0,0,0.8)';
ctx.shadowBlur = 8;
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
} else {
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
}
let currentY = startY;
for (const l of lines) {
ctx.fillText(l, x, currentY);
currentY += lineHeight;
}
ctx.shadowColor = 'transparent'; // Reset shadow
return currentY; // Return Y position after the last line
};
// 6. Define text properties and draw text elements
const marginX = coverWidth * 0.08;
const contentMaxWidth = coverWidth - 2 * marginX;
let currentYPos = coverHeight * 0.12;
// Tagline
if (tagline && tagline.trim() !== "") {
const taglineText = tagline.toUpperCase();
const taglineFontSize = Math.floor(coverWidth / 32);
const isStylisticFont = ["Nosifer", "Creepster", "Metal Mania"].includes(primaryFontForLoading);
const taglineFontFamily = isStylisticFont ? '"Georgia", Times, serif' : fontName;
const taglineFontStyle = `italic ${taglineFontSize}px ${taglineFontFamily}`;
const taglineLines = getLinesForText(taglineText, contentMaxWidth * 0.85, taglineFontStyle);
currentYPos = drawTextLines(taglineLines, coverWidth / 2, currentYPos, taglineFontSize * 1.3, taglineFontStyle, secondaryColor, false);
currentYPos += coverHeight * 0.04;
} else {
currentYPos = coverHeight * 0.20;
}
// Title
const titleText = title.toUpperCase();
const titleFontSize = Math.floor(coverWidth / Math.max(7, titleText.length / 2.5)); // Adjust size based on length
const titleFontStyle = `bold ${titleFontSize}px ${fontName}`;
const titleLineHeight = titleFontSize * 1.05;
currentYPos = Math.max(currentYPos, coverHeight * (tagline && tagline.trim() !== "" ? 0.18 : 0.22));
const titleLines = getLinesForText(titleText, contentMaxWidth, titleFontStyle);
currentYPos = drawTextLines(titleLines, coverWidth / 2, currentYPos, titleLineHeight, titleFontStyle, primaryColor, true);
currentYPos += coverHeight * 0.05;
// Author
const authorText = author.toUpperCase();
const authorFontSize = Math.floor(coverWidth / 24);
const authorFontStyle = `normal ${authorFontSize}px ${fontName}`; // Use 'normal' weight explicitly
const authorLineHeight = authorFontSize * 1.2;
const authorLines = getLinesForText(authorText, contentMaxWidth * 0.9, authorFontStyle);
const authorBlockHeight = authorLines.length * authorLineHeight;
// Try to place author in lower ~15-25% of cover, ensuring it's after title.
let authorY = Math.max(currentYPos + coverHeight * 0.05, coverHeight * 0.93 - authorBlockHeight);
authorY = Math.min(authorY, coverHeight * 0.93 - authorBlockHeight); // Ensure it fits from bottom
drawTextLines(authorLines, coverWidth / 2, authorY, authorLineHeight, authorFontStyle, secondaryColor, true);
return canvas;
}
Apply Changes