You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
coverColor = "#4B0082", // Dark purple/indigo for a magical feel
borderColor = "#FFD700", // Gold for richness
spineTitleText = "Tome of Secrets",
spineTitleColor = "#FFFFE0", // Light Yellow / Ivory for text visibility
pageColor = "#F5F5DC", // Beige for aged paper
pageLineColor = "rgba(0,0,0,0.12)", // Subtle lines for pages
coverAccentColor = "#DAA520", // Goldenrod (darker gold for corners/accents)
coverTitleText = "", // Optional title on the front cover, empty by default
coverTitleFont = "30px 'MedievalSharp', Times, serif", // Font for cover title
coverTitleColor = "#FFFFE0", // Color for cover title
spineTitleFont = "20px 'MedievalSharp', Times, serif" // Font for spine title
) {
// Helper function to load custom font dynamically using FontFace API
async function loadCustomFont(fontFamily, fontUrl, fontWeight = 'normal', fontStyle = 'normal') {
const fontId = `${fontFamily}-${fontWeight}-${fontStyle}`;
if (!window.loadedFonts) {
window.loadedFonts = {}; // Cache to track loaded fonts globally
}
// If already attempted and succeeded/failed, or font is already available system-wide
if (window.loadedFonts[fontId] === 'loaded' || document.fonts.check(`12px "${fontFamily}"`, { weight: fontWeight, style: fontStyle })) {
if (!document.fonts.check(`12px "${fontFamily}"`, { weight: fontWeight, style: fontStyle })) {
// It was marked loaded but check fails. Maybe cleared from document.fonts. Try reloading.
} else {
return true;
}
}
if (window.loadedFonts[fontId] === 'failed') return false;
try {
const fontFace = new FontFace(fontFamily, `url(${fontUrl})`, {
weight: fontWeight,
style: fontStyle,
display: 'swap', // Use swap for better perceived performance
});
await fontFace.load();
document.fonts.add(fontFace);
window.loadedFonts[fontId] = 'loaded';
return true;
} catch (e) {
console.warn(`Failed to load font '${fontFamily}' (${fontWeight}, ${fontStyle}) from ${fontUrl}:`, e);
window.loadedFonts[fontId] = 'failed';
return false;
}
}
// Load the 'MedievalSharp' font. It's a Google Font suitable for a magical tome.
// Using the direct .woff2 URL for 'MedievalSharp Regular 400'.
const medievalSharpFontUrl = 'https://fonts.gstatic.com/s/medievalsharp/v21/EvNMgfuHkjOK_S9N42Xbf-bFquHXAIlMzw.woff2';
await loadCustomFont('MedievalSharp', medievalSharpFontUrl, '400', 'normal');
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const canvasWidth = 600;
const canvasHeight = 800;
canvas.width = canvasWidth;
canvas.height = canvasHeight;
// Book dimensions and positioning
const bookTotalHeight = 700;
const bookTotalWidth = 500;
const bookX = (canvasWidth - bookTotalWidth) / 2;
const bookY = (canvasHeight - bookTotalHeight) / 2;
const spineWidth = 50;
const pageEdgeWidth = 25;
const coverOverhangVertical = 10; // How much cover extends beyond pages vertically
const frontCoverPanelWidth = bookTotalWidth - spineWidth - pageEdgeWidth;
const pageBlockVisualY = bookY + coverOverhangVertical;
const pageBlockVisualHeight = bookTotalHeight - 2 * coverOverhangVertical;
// 1. Draw Page Edges (Right Side)
const pageEdgeX = bookX + bookTotalWidth - pageEdgeWidth;
ctx.fillStyle = pageColor;
ctx.fillRect(pageEdgeX, pageBlockVisualY, pageEdgeWidth, pageBlockVisualHeight);
// Draw lines to simulate pages
ctx.strokeStyle = pageLineColor;
ctx.lineWidth = 0.5;
for (let y_ = pageBlockVisualY; y_ < pageBlockVisualY + pageBlockVisualHeight; y_ += 3) {
ctx.beginPath();
ctx.moveTo(pageEdgeX, y_);
ctx.lineTo(pageEdgeX + pageEdgeWidth, y_);
ctx.stroke();
}
// Add a subtle shadow to make pages look inset from the cover edge
ctx.fillStyle = 'rgba(0,0,0,0.05)';
ctx.fillRect(pageEdgeX, pageBlockVisualY, pageEdgeWidth, pageBlockVisualHeight);
// 2. Draw Spine (Left Side)
ctx.fillStyle = coverColor;
ctx.fillRect(bookX, bookY, spineWidth, bookTotalHeight); // Base spine color
// Spine shading for a rounded effect (using translucent overlays)
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'; // Darker shade
ctx.fillRect(bookX, bookY, spineWidth * 0.4, bookTotalHeight);
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)'; // Lighter highlight
ctx.fillRect(bookX + spineWidth * 0.6, bookY, spineWidth * 0.4, bookTotalHeight);
// 3. Draw Front Cover Panel
const fcPanelX = bookX + spineWidth;
ctx.fillStyle = coverColor;
ctx.fillRect(fcPanelX, bookY, frontCoverPanelWidth, bookTotalHeight);
// 4. Front Cover Decorations
// Border
const borderMargin = 20;
const borderWidth = 6;
ctx.strokeStyle = borderColor;
ctx.lineWidth = borderWidth;
const innerBorderX = fcPanelX + borderMargin;
const innerBorderY = bookY + borderMargin;
const innerBorderW = frontCoverPanelWidth - 2 * borderMargin;
const innerBorderH = bookTotalHeight - 2 * borderMargin;
if (innerBorderW > 0 && innerBorderH > 0) {
ctx.strokeRect(innerBorderX + borderWidth/2, innerBorderY + borderWidth/2, innerBorderW - borderWidth, innerBorderH - borderWidth);
}
// Corner Decorations
const cornerRadius = 25;
ctx.fillStyle = coverAccentColor;
if (innerBorderW > cornerRadius * 2 && innerBorderH > cornerRadius * 2) {
// Top-left
ctx.beginPath(); ctx.moveTo(innerBorderX, innerBorderY + cornerRadius);
ctx.arcTo(innerBorderX, innerBorderY, innerBorderX + cornerRadius, innerBorderY, cornerRadius);
ctx.lineTo(innerBorderX, innerBorderY); ctx.closePath(); ctx.fill();
// Top-right
ctx.beginPath(); ctx.moveTo(innerBorderX + innerBorderW - cornerRadius, innerBorderY);
ctx.arcTo(innerBorderX + innerBorderW, innerBorderY, innerBorderX + innerBorderW, innerBorderY + cornerRadius, cornerRadius);
ctx.lineTo(innerBorderX + innerBorderW, innerBorderY); ctx.closePath(); ctx.fill();
// Bottom-left
ctx.beginPath(); ctx.moveTo(innerBorderX, innerBorderY + innerBorderH - cornerRadius);
ctx.arcTo(innerBorderX, innerBorderY + innerBorderH, innerBorderX + cornerRadius, innerBorderY + innerBorderH, cornerRadius);
ctx.lineTo(innerBorderX, innerBorderY + innerBorderH); ctx.closePath(); ctx.fill();
// Bottom-right
ctx.beginPath(); ctx.moveTo(innerBorderX + innerBorderW - cornerRadius, innerBorderY + innerBorderH);
ctx.arcTo(innerBorderX + innerBorderW, innerBorderY + innerBorderH, innerBorderX + innerBorderW, innerBorderY + innerBorderH - cornerRadius, cornerRadius);
ctx.lineTo(innerBorderX + innerBorderW, innerBorderY + innerBorderH); ctx.closePath(); ctx.fill();
}
// 5. Draw originalImg on Front Cover
const imgPaddingFromBorder = Math.max(5, cornerRadius / 2 + 5) ; // Ensure some padding even without corners
const imgAvailableX = innerBorderX + imgPaddingFromBorder;
const imgAvailableY = innerBorderY + imgPaddingFromBorder;
const imgAvailableW = innerBorderW - 2 * imgPaddingFromBorder;
const imgAvailableH = innerBorderH - 2 * imgPaddingFromBorder;
let imgDrawn = false; // Flag to track if image was drawn
if (imgAvailableW > 0 && imgAvailableH > 0 && originalImg && originalImg.width > 0 && originalImg.height > 0) {
const imgAspect = originalImg.width / originalImg.height;
let drawW = imgAvailableW;
let drawH = drawW / imgAspect;
if (drawH > imgAvailableH) {
drawH = imgAvailableH;
drawW = drawH * imgAspect;
}
// Center the image
const imgDrawX = imgAvailableX + (imgAvailableW - drawW) / 2;
const imgDrawY = imgAvailableY + (imgAvailableH - drawH) / 2;
try {
ctx.drawImage(originalImg, imgDrawX, imgDrawY, drawW, drawH);
imgDrawn = true; // Set flag
} catch (e) {
console.warn("Error drawing image, it might not be fully loaded or is invalid:", e);
}
}
// 6. Draw Spine Title
if (spineTitleText && spineTitleText.trim() !== "") {
ctx.font = spineTitleFont;
ctx.fillStyle = spineTitleColor;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.save();
ctx.translate(bookX + spineWidth / 2, bookY + bookTotalHeight / 2);
ctx.rotate(-Math.PI / 2); // Rotate text for spine
ctx.fillText(spineTitleText, 0, 0);
ctx.restore();
}
// 7. Draw Cover Title (if provided)
if (coverTitleText && coverTitleText.trim() !== "") {
ctx.font = coverTitleFont;
ctx.fillStyle = coverTitleColor;
ctx.textAlign = "center";
let titleY;
const metrics = ctx.measureText("M"); // A character to estimate height
const titleHeightEstimate = (metrics.actualBoundingBoxAscent || parseInt(ctx.font, 10) * 0.75) + (metrics.actualBoundingBoxDescent || parseInt(ctx.font, 10) * 0.25) ;
if (imgDrawn) {
const imgBottomY = (imgAvailableY + (imgAvailableH - (innerBorderW / (originalImg.width / originalImg.height) > imgAvailableH ? imgAvailableH : innerBorderW / (originalImg.width / originalImg.height)))/2 ) + (innerBorderW / (originalImg.width / originalImg.height) > imgAvailableH ? imgAvailableH : innerBorderW / (originalImg.width / originalImg.height));
const spaceBelowImage = (innerBorderY + innerBorderH) - imgBottomY;
if (spaceBelowImage > titleHeightEstimate + 10) { // Prefer below image
titleY = imgBottomY + 10 ;
ctx.textBaseline = "top";
} else { // Try above image
const imgTopY = imgAvailableY + (imgAvailableH - (innerBorderW / (originalImg.width / originalImg.height) > imgAvailableH ? imgAvailableH : innerBorderW / (originalImg.width / originalImg.height)))/2 ;
if (imgTopY - innerBorderY > titleHeightEstimate + 10) {
titleY = imgTopY - 10;
ctx.textBaseline = "bottom";
} else { // Fallback: center of image area
titleY = imgAvailableY + imgAvailableH / 2;
ctx.textBaseline = "middle";
}
}
} else { // No image, center title in the available decorated area
titleY = innerBorderY + innerBorderH / 2;
ctx.textBaseline = "middle";
}
if (innerBorderW > 0) { // Only draw if there's space
ctx.fillText(coverTitleText, innerBorderX + innerBorderW / 2, titleY);
}
}
return canvas;
}
Apply Changes