You can edit the below JavaScript code to customize the image tool.
async function processImage(originalImg,
titleText = "Whispers of Desire",
authorText = "By A. N. Romancer",
taglineText = "A love that defied all odds.", // Set to "" or null to hide tagline
titleFontFamily = "Great Vibes", // A romantic script font. Ensure URL points to its woff2 file.
titleFontUrl = "https://fonts.gstatic.com/s/greatvibes/v18/RWmMoKWR9v4hfMFdnkLqCohenjI.woff2",
authorFontFamily = "Cinzel", // An elegant serif font. Ensure URL points to its woff2 file.
authorFontUrl = "https://fonts.gstatic.com/s/cinzel/v20/8vIJ7ww63mVu7gt7FfgrkFcHoA.woff2",
taglineFontFamily = "Roboto Condensed", // A clean, readable sans-serif.
taglineFontUrl = "https://fonts.gstatic.com/s/robotocondensed/v27/ieVo2ZhZI2eCN5iU1GcYvgEigLs8Bqi4An-_rA.woff2",
titleColor = "#FFD700", // Gold color for richness
authorColor = "#FFFFFF", // Classic white
taglineColor = "#F0F0F0", // Slightly off-white
shadowColor = "rgba(0,0,0,0.7)",
shadowBlur = 7,
shadowOffsetX = 2,
shadowOffsetY = 2,
overlayColor = "rgba(50,20,70,0.25)", // Dark moody purple overlay; "transparent" or "" for none
vignetteStrength = 0.7, // Range 0 (none) to 1 (strong). Affects darkness and spread.
targetCoverWidth = 800, // Output canvas width in pixels
targetCoverAspectRatio = 1.6 // Height / Width (e.g., 1.6 for a 6x9.6 inch equivalent)
) {
// Validate originalImg
if (!originalImg || typeof originalImg.naturalWidth === 'undefined' || originalImg.naturalWidth === 0 || originalImg.naturalHeight === 0) {
console.error("Original image is not loaded or invalid. Ensure originalImg is a loaded HTMLImageElement.");
const errorCanvas = document.createElement('canvas');
errorCanvas.width = targetCoverWidth;
errorCanvas.height = targetCoverWidth * targetCoverAspectRatio;
const errCtx = errorCanvas.getContext('2d');
errCtx.fillStyle = 'lightgray';
errCtx.fillRect(0,0, errorCanvas.width, errorCanvas.height);
errCtx.fillStyle = 'red';
errCtx.font = `${Math.round(targetCoverWidth*0.03)}px Arial`;
errCtx.textAlign = 'center';
errCtx.textBaseline = 'middle';
errCtx.fillText("Error: Image not loaded or invalid.", errorCanvas.width/2, errorCanvas.height/2);
return errorCanvas;
}
// Initialize font loading cache on the function itself if not already present
// This cache persists across multiple calls to processImage
if (!processImage.fontLoadPromises) {
processImage.fontLoadPromises = {};
}
// Helper function to load web fonts (nested to be part of the main function block)
async function loadWebFont_internal(fontFamily, fontUrl) {
if (!fontFamily || !fontUrl) return false; // No font/URL specified, treated as no-op for this font
if (document.fonts) { // Check for Font Loading API support
// Check if font is already system-available or globally loaded and tracked by document.fonts
try { // Enclose document.fonts access in try-catch for stricter environments (e.g. sandboxed iframes)
if (document.fonts.check(`1em "${fontFamily}"`)) {
// console.log(`Font "${fontFamily}" is already available (system or CSS-loaded).`);
return true;
}
} catch(e) {
// console.warn("Could not check document.fonts:", e);
}
// Check our own loading cache
if (processImage.fontLoadPromises[fontFamily]) {
// console.log(`Waiting for existing load promise of "${fontFamily}".`);
return processImage.fontLoadPromises[fontFamily]; // Return existing promise
}
// Create and cache the font loading promise
// console.log(`Attempting to load font "${fontFamily}" from ${fontUrl}`);
const fontFace = new FontFace(fontFamily, `url(${fontUrl})`);
processImage.fontLoadPromises[fontFamily] = fontFace.load().then(loadedFace => {
document.fonts.add(loadedFace);
// console.log(`Font "${fontFamily}" loaded successfully.`);
return true;
}).catch(e => {
console.error(`Failed to load font '${fontFamily}' from ${fontUrl}:`, e);
delete processImage.fontLoadPromises[fontFamily]; // Remove failed promise to allow retry
return false; // Indicate failure
});
return processImage.fontLoadPromises[fontFamily];
}
// Fallback if document.fonts API is not supported
console.warn("document.fonts API not supported by this browser. Fonts may not load as expected.");
return false; // Indicate that font loading might not occur
}
// Text wrapping helper function (nested)
function wrapText_internal(context, text, x, yCenter, maxWidth, lineHeight) {
if (!text || text.trim() === "") return;
const words = text.split(' ');
let line = '';
let lines = [];
for (let n = 0; n < words.length; n++) {
let testLine = line + words[n] + ' ';
// The font must be set on the context BEFORE calling measureText
let metrics = context.measureText(testLine);
let testWidth = metrics.width;
if (testWidth > maxWidth && n > 0) { // If the line exceeds max width (and it's not the first word)
lines.push(line.trim()); // Add the current line (without the new word)
line = words[n] + ' '; // Start a new line with the current word
} else {
line = testLine; // Otherwise, add the word to the current line
}
}
lines.push(line.trim()); // Add the last line
context.textBaseline = 'middle'; // Vertically align text nicely
// Calculate the starting Y position to center the entire text block vertically at yCenter
const totalTextBlockHeight = (lines.length - 1) * lineHeight;
let currentY = yCenter - totalTextBlockHeight / 2;
for (let i = 0; i < lines.length; i++) {
context.fillText(lines[i], x, currentY);
currentY += lineHeight; // Move to the next line position
}
}
// Load all necessary fonts concurrently.
// Using await Promise.all ensures all font loading attempts are made before drawing.
// Individual failures are handled within loadWebFont_internal (logged, returns false).
// The browser will use fallback fonts specified in ctx.font if custom fonts fail.
await Promise.all([
loadWebFont_internal(titleFontFamily, titleFontUrl),
loadWebFont_internal(authorFontFamily, authorFontUrl),
loadWebFont_internal(taglineFontFamily, taglineFontUrl)
]);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Set canvas dimensions based on target width and aspect ratio
canvas.width = targetCoverWidth;
canvas.height = Math.round(targetCoverWidth * targetCoverAspectRatio);
// --- Draw Background Image (cropped/scaled to fit target aspect ratio) ---
const imgAspectRatio = originalImg.naturalHeight / originalImg.naturalWidth;
let sx = 0, sy = 0, sWidth = originalImg.naturalWidth, sHeight = originalImg.naturalHeight;
if (imgAspectRatio > targetCoverAspectRatio) { // Image is taller/slimmer than target
sHeight = originalImg.naturalWidth * targetCoverAspectRatio; // Calculate new source height to match target AR
sy = (originalImg.naturalHeight - sHeight) / 2; // Center crop vertically
} else if (imgAspectRatio < targetCoverAspectRatio) { // Image is wider/shorter than target
sWidth = originalImg.naturalHeight / targetCoverAspectRatio; // Calculate new source width
sx = (originalImg.naturalWidth - sWidth) / 2; // Center crop horizontally
}
// Draw the (potentially cropped)_alignment source image_alignment onto the entire canvas
ctx.drawImage(originalImg, sx, sy, sWidth, sHeight, 0, 0, canvas.width, canvas.height);
// --- Apply Color Overlay ---
if (overlayColor && overlayColor !== "transparent" && overlayColor !== "" && overlayColor !== "none") {
ctx.fillStyle = overlayColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
// --- Apply Vignette Effect ---
if (vignetteStrength > 0) {
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
// Outer radius should extend slightly beyond canvas corners to ensure full coverage
const outerRadius = Math.sqrt(centerX*centerX + centerY*centerY) * 1.1;
// Inner radius of the transparent part: shrinks as strength increases
// The 0.7 and 0.65 factors control the gradient spread and central light area size
const innerRadiusPercent = Math.max(0.05, 0.7 - vignetteStrength * 0.65);
const innerRadius = outerRadius * innerRadiusPercent;
const gradient = ctx.createRadialGradient(centerX, centerY, innerRadius, centerX, centerY, outerRadius);
gradient.addColorStop(0, 'rgba(0,0,0,0)'); // Center is transparent
// Edge darkness increases with strength
gradient.addColorStop(1, `rgba(0,0,0,${Math.min(1, vignetteStrength)})`);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
// --- Text Drawing ---
// Common text properties
ctx.textAlign = 'center'; // All text blocks will be centered horizontally
ctx.shadowColor = shadowColor;
ctx.shadowBlur = shadowBlur;
ctx.shadowOffsetX = shadowOffsetX;
ctx.shadowOffsetY = shadowOffsetY;
const padding = canvas.width * 0.05; // 5% padding from canvas edges for text
const maxTextWidth = canvas.width - 2 * padding; // Max width for any text block
// Draw Tagline (if provided)
if (taglineText && taglineText.trim() !== "") {
const taglineFontSize = Math.round(canvas.height * 0.035);
const taglineLineHeight = taglineFontSize * 1.3; // Line height relative to font size
// Set font: Size, Family, Generic Fallback
ctx.font = `${taglineFontSize}px "${taglineFontFamily}", sans-serif`;
ctx.fillStyle = taglineColor;
// Y position: 18% from the top of the canvas for the center of the tagline block
wrapText_internal(ctx, taglineText, canvas.width / 2, canvas.height * 0.18, maxTextWidth, taglineLineHeight);
}
// Draw Title
const titleFontSize = Math.round(canvas.height * 0.12); // Larger font for title
const titleLineHeight = titleFontSize * 1.35; // Script fonts might need slightly more line height
ctx.font = `${titleFontSize}px "${titleFontFamily}", cursive`; // Cursive fallback
ctx.fillStyle = titleColor;
// Y position: 42% from the top for the center of the title block
wrapText_internal(ctx, titleText, canvas.width / 2, canvas.height * 0.42, maxTextWidth, titleLineHeight);
// Draw Author Name
const authorFontSize = Math.round(canvas.height * 0.055);
const authorLineHeight = authorFontSize * 1.3;
ctx.font = `${authorFontSize}px "${authorFontFamily}", serif`; // Serif fallback
ctx.fillStyle = authorColor;
// Y position: 86% from the top (towards the bottom) for the center of the author block
wrapText_internal(ctx, authorText, canvas.width / 2, canvas.height * 0.86, maxTextWidth, authorLineHeight);
// Reset shadow for any subsequent drawing operations if needed elsewhere (good practice)
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
return canvas;
}
Free Image Tool Creator
Can't find the image tool you're looking for? Create one based on your own needs now!
The Image Romance Novel Cover Template Creator is a versatile tool that allows users to design customized covers for romance novels. With this tool, you can upload an image of your choice, add personalized text elements such as the title, author name, and an optional tagline, and apply various stylistic enhancements including font selection, colors, and text shadows. This tool is ideal for self-publishing authors, graphic designers, and anyone looking to create eye-catching book covers that stand out in a crowded market. Whether for print or digital formats, the template creator enables you to produce professional-quality covers to effectively capture readers’ attention.