You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, name = "Mysterious Stranger", crime = "Causing a Tavern Brawl", rewardAmount = "100 Gold Pieces", status = "Dead or Alive", fontNameParam = "IM Fell DW Pica", posterColor = "#F5DEB3", textColor = "#5C4033", imageEffect = "sepia") {
// FONT LOADING HELPER
async function ensureFontIsAvailable(fontFamilyToLoad, googleFontCssUrl = null) {
let isFontLoaded = false;
try {
if (document.fonts && typeof document.fonts.values === 'function') {
for (const fontFace of document.fonts.values()) {
if (fontFace.family === fontFamilyToLoad) {
if (fontFace.status === 'loaded') {
isFontLoaded = true;
break;
} else if (fontFace.status === 'unloaded' || fontFace.status === 'loading') {
await fontFace.load();
if (fontFace.status === 'loaded') {
isFontLoaded = true;
break;
}
}
}
}
}
} catch(e) {
// console.warn("Error accessing document.fonts: ", e);
}
if (!isFontLoaded && document.fonts && typeof document.fonts.check === 'function') {
if (document.fonts.check(`12px "${fontFamilyToLoad}"`)) { // Check with a size and quotes
isFontLoaded = true;
}
}
if (isFontLoaded) {
return fontFamilyToLoad;
}
if (googleFontCssUrl) {
try {
const linkId = `dynamic-font-stylesheet-${fontFamilyToLoad.replace(/\s+/g, '-')}`;
if (!document.getElementById(linkId)) {
const link = document.createElement('link');
link.id = linkId;
link.href = googleFontCssUrl;
link.rel = 'stylesheet';
const p = new Promise((resolve, reject) => {
link.onload = resolve;
link.onerror = () => reject(new Error(`Failed to load stylesheet ${googleFontCssUrl}`));
document.head.appendChild(link);
});
await p;
}
if (document.fonts && typeof document.fonts.load === 'function') {
await document.fonts.load(`12px "${fontFamilyToLoad}"`); // Load with a style string
if (document.fonts.check(`12px "${fontFamilyToLoad}"`)) {
return fontFamilyToLoad;
}
} else if (document.fonts && document.fonts.check && document.fonts.check(`12px "${fontFamilyToLoad}"`)) {
return fontFamilyToLoad; // Fallback check if load() not supported but check() is
}
} catch (e) {
console.warn(`Failed to load font "${fontFamilyToLoad}" from URL: ${googleFontCssUrl}. Error: ${e}`);
}
}
// console.warn(`Using "serif" as fallback for "${fontFamilyToLoad}".`);
return "serif"; // Fallback font
}
const effectiveFontFamily = await ensureFontIsAvailable(
fontNameParam,
fontNameParam === "IM Fell DW Pica" ? "https://fonts.googleapis.com/css2?family=IM+Fell+DW+Pica:ital@0;1&display=swap" : null
);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const posterWidth = 600;
const posterHeight = 900;
canvas.width = posterWidth;
canvas.height = posterHeight;
const padding = 40;
// Draw Background
ctx.fillStyle = posterColor;
ctx.fillRect(0, 0, posterWidth, posterHeight);
// Optional: Draw a subtle, darker "aged" border inset from the edge
ctx.strokeStyle = "rgba(0,0,0,0.1)"; // Darker, semi-transparent color for aged effect
ctx.lineWidth = padding * 0.75; // Make it fairly thick
// Draw rectangle slightly inset, so lineWidth creates border towards center
ctx.strokeRect(ctx.lineWidth / 2, ctx.lineWidth / 2, posterWidth - ctx.lineWidth, posterHeight - ctx.lineWidth);
ctx.lineWidth = 1; // Reset line width for other drawings
// Text Drawing Helper Functions
function drawText(text, x, y, fontStyleWeight, fontSize, color, align = 'center', maxWidth = posterWidth - 2 * padding) {
ctx.font = `${fontStyleWeight} ${fontSize}px "${effectiveFontFamily}", serif`;
ctx.fillStyle = color;
ctx.textAlign = align;
ctx.textBaseline = 'top'; // All y-coordinates for text will be their top edge
ctx.fillText(text, x, y, maxWidth);
}
function drawWrappedText(text, x, y, fontStyleWeight, fontSize, color, align, maxWidth, lineHeight) {
ctx.font = `${fontStyleWeight} ${fontSize}px "${effectiveFontFamily}", serif`;
ctx.fillStyle = color;
ctx.textAlign = align;
ctx.textBaseline = 'top';
const words = text.split(' ');
let line = '';
let currentLineY = y; // Y for the top of the current line being built
for (let n = 0; n < words.length; n++) {
const testLine = line + words[n] + ' ';
const metrics = ctx.measureText(testLine);
const testWidth = metrics.width;
if (testWidth > maxWidth && n > 0) {
ctx.fillText(line.trim(), x, currentLineY);
line = words[n] + ' ';
currentLineY += lineHeight;
} else {
line = testLine;
}
}
ctx.fillText(line.trim(), x, currentLineY); // Draw the last line
return currentLineY + lineHeight; // Return the Y-coordinate for the top of the *next* element
}
// --- Poster Layout and Drawing ---
let currentY = padding; // Start current Y position from top padding
// "WANTED" Text
const wantedFontSize = 70;
drawText("WANTED", posterWidth / 2, currentY, "bold", wantedFontSize, textColor);
currentY += wantedFontSize + 10; // Advance Y by font size + margin
// "Status" Text (e.g., Dead or Alive)
const statusFontSize = 20;
drawText(status.toUpperCase(), posterWidth / 2, currentY, "normal", statusFontSize, textColor);
currentY += statusFontSize + 20; // Advance Y by font size + margin before image
// --- Image Section ---
const imgTop = currentY; // Top boundary for the image's container
const imgContainerX = padding;
// Estimate space needed for text elements below the image
const spaceForBottomText = name.length > 25 ? 280 : 240; // Name, "For", Crime, "Reward", Amount + margins
// (dynamic adjustments could be more complex)
let imgContainerHeight = posterHeight - imgTop - spaceForBottomText - padding;
// Constrain image container height
const minImgHeight = posterHeight * 0.20; // Minimum 20% of poster height
const maxImgHeight = posterHeight * 0.45; // Maximum 45% of poster height
if (imgContainerHeight < minImgHeight) imgContainerHeight = minImgHeight;
if (imgContainerHeight > maxImgHeight) imgContainerHeight = maxImgHeight;
const imgContainerWidth = posterWidth - 2 * padding;
let drawWidth, drawHeight, dx, dy;
const imgNaturalWidth = originalImg.naturalWidth || originalImg.width; // Ensure we have a width
const imgNaturalHeight = originalImg.naturalHeight || originalImg.height; // Ensure we have a height
if (imgNaturalWidth > 0 && imgNaturalHeight > 0) { // Only process if image has valid dimensions
const imgAspect = imgNaturalWidth / imgNaturalHeight;
if (imgNaturalWidth > imgContainerWidth || imgNaturalHeight > imgContainerHeight) { // Image needs scaling
const containerAspect = imgContainerWidth / imgContainerHeight;
if (imgAspect > containerAspect) { // Image is wider relative to container
drawWidth = imgContainerWidth;
drawHeight = drawWidth / imgAspect;
} else { // Image is taller or same aspect relative to container
drawHeight = imgContainerHeight;
drawWidth = drawHeight * imgAspect;
}
} else { // Image is smaller than container, draw it at its original size
drawWidth = imgNaturalWidth;
drawHeight = imgNaturalHeight;
}
} else { // Fallback if image has no dimensions (e.g., not loaded, though prompt implies it is)
console.warn("Original image has zero dimensions. Drawing placeholder.");
drawWidth = imgContainerWidth * 0.8; // Placeholder size
drawHeight = imgContainerHeight * 0.8;
}
// Center the scaled image within its allocated container space
dx = imgContainerX + (imgContainerWidth - drawWidth) / 2;
dy = imgTop + (imgContainerHeight - drawHeight) / 2;
// Draw Image with effect
ctx.save();
if (imageEffect === "sepia") ctx.filter = "sepia(1)";
else if (imageEffect === "grayscale") ctx.filter = "grayscale(1)";
// else: "none" or other, no filter applied
ctx.drawImage(originalImg, dx, dy, drawWidth, drawHeight);
ctx.filter = "none"; // Reset filter for subsequent drawings
ctx.restore();
// Draw Border around image
ctx.strokeStyle = textColor;
ctx.lineWidth = 3;
ctx.strokeRect(dx - 5, dy - 5, drawWidth + 10, drawHeight + 10); // Slightly padded border
ctx.lineWidth = 1; // Reset line width
// Update currentY to be below the image frame (dy is top of image, drawHeight is its height)
currentY = dy + drawHeight + 10 + 20; // 10 for border, 20 for margin
// --- Text Below Image ---
// Person's Name
const nameFontSize = Math.min(40, Math.floor( (posterWidth - 2 * padding) / (name.length * 0.6) ) +4 ); // Auto-adjust size a bit
drawText(name, posterWidth / 2, currentY, "bold", nameFontSize, textColor, 'center', posterWidth - padding); // Wider max width for name
currentY += nameFontSize + 10;
// "FOR THE CRIME OF:" Text
const forCrimeFontSize = 18;
drawText("FOR THE CRIME OF:", posterWidth / 2, currentY, "italic", forCrimeFontSize, textColor);
currentY += forCrimeFontSize + 10;
// Crime Description (Wrapped)
const crimeFontSize = 26;
const crimeLineHeight = crimeFontSize * 1.2;
currentY = drawWrappedText(crime, posterWidth / 2, currentY, "normal", crimeFontSize, textColor, 'center', posterWidth - 2 * padding - 20, crimeLineHeight);
// currentY from drawWrappedText is already top of next element. Add a margin.
currentY += 20;
// --- Reward Section (attempt to position towards bottom) ---
const rewardSectionMinY = currentY; // Reward section cannot start above this Y
const rewardTitleFontSize = 50;
const rewardAmountFontSize = 30;
// Calculate total height needed for the reward section texts plus spacing
const rewardSectionVisualHeight = rewardTitleFontSize + 10 + rewardAmountFontSize;
// Attempt to position the reward section so its bottom aligns with poster bottom minus padding
currentY = posterHeight - padding - rewardSectionVisualHeight;
// Ensure reward section doesn't overlap with content above it
if (currentY < rewardSectionMinY) {
currentY = rewardSectionMinY; // If overlap, place it right after previous content
}
// "REWARD" Text
drawText("REWARD", posterWidth / 2, currentY, "bold", rewardTitleFontSize, textColor);
currentY += rewardTitleFontSize + 10; // Advance Y
// Reward Amount Text
drawText(rewardAmount, posterWidth / 2, currentY, "normal", rewardAmountFontSize, textColor);
// currentY += rewardAmountFontSize; // No further text below this
return canvas;
}
Apply Changes