You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
bountyName = "MYSTERIOUS OUTLAW",
rewardAmount = "GALACTIC CREDITS: 1,000,000",
wantedText = "WANTED",
subText = "DEAD OR ALIVE",
crimeDescription = "For crimes against the Galactic Concord, including unauthorized warp jumps, smuggling rare space-emeralds, and telling bad jokes at the Interstellar Comedy Club.",
posterWidth = 600,
posterHeight = 900,
backgroundColor = "#EADCB8", // Parchment paper-like color
textColor = "#3A2D22", // Dark brown, aged text color
headlineFont = "Ultra", // A bold Google Font for "WANTED"
bodyFont = "Special Elite", // A typewriter-style Google Font for other text
imageBorderColor = "#2F251B", // Darker border for the image
imageBorderWidth = 4,
applySepiaToImage = 1, // 1 for true (apply sepia), 0 for false
applyNoiseToBackground = 1, // 1 for true (add noise), 0 for false
noiseIntensity = 0.1 // Noise intensity (0.0 to 1.0)
) {
// --- Helper Function: Font Loader ---
// Loads a font from Google Fonts, with fallbacks.
function _loadFont(fontFamily) {
const fontId = `dynamic-font-${fontFamily.replace(/\s+/g, '-')}`;
if (document.fonts.check(`12px "${fontFamily}"`)) {
return Promise.resolve(); // Font already available (system or previously loaded)
}
if (document.getElementById(fontId)) { // Link tag already added
return document.fonts.load(`12px "${fontFamily}"`).catch(() => {
// Font loading might still fail, but promise resolves for fallback.
});
}
return new Promise((resolve) => {
const link = document.createElement('link');
link.id = fontId;
link.href = `https://fonts.googleapis.com/css2?family=${fontFamily.replace(/\s+/g, '+')}:wght@400&display=swap`;
link.rel = 'stylesheet';
link.onload = () => {
document.fonts.load(`12px "${fontFamily}"`)
.then(resolve) // Font face loaded
.catch(() => {
console.warn(`Font face for ${fontFamily} failed to load after CSS was fetched.`);
resolve(); // Resolve anyway for fallback logic
});
};
link.onerror = (err) => {
console.warn(`Failed to load Google Font CSS for "${fontFamily}":`, err);
resolve(); // Resolve for fallback logic
};
document.head.appendChild(link);
});
}
// --- Helper Function: Wrap Text ---
// Draws multi-line text, centered, within a max width.
function _wrapText(context, text, x, y, maxWidth, lineHeight, fontName, fallbackFont) {
const effectiveFont = document.fonts.check(`1em "${fontName}"`) ? fontName : fallbackFont;
context.font = `${lineHeight}px "${effectiveFont}"`;
context.textAlign = "center";
const words = text.split(' ');
let line = '';
let currentY = y;
const lineSpacing = lineHeight * 1.2; // 1.2 provides decent spacing
for (let n = 0; n < words.length; n++) {
const testLine = line + words[n] + ' ';
const metrics = context.measureText(testLine);
const testWidth = metrics.width;
if (testWidth > maxWidth && n > 0) {
context.fillText(line.trim(), x, currentY);
line = words[n] + ' ';
currentY += lineSpacing;
} else {
line = testLine;
}
}
context.fillText(line.trim(), x, currentY);
}
// 1. Load Fonts asynchronously
// Catch individual font loading errors to prevent Promise.all from rejecting prematurely.
await Promise.all([
_loadFont(headlineFont).catch(e => console.warn(`Error loading headline font ${headlineFont}:`, e)),
_loadFont(bodyFont).catch(e => console.warn(`Error loading body font ${bodyFont}:`, e))
]);
// Determine effective fonts to use (with fallbacks if custom fonts failed to load)
const FONT_HEADLINE_FALLBACK = "Georgia, serif"; // A common serif font
const FONT_BODY_FALLBACK = "Courier New, monospace"; // A common monospace font
const effectiveHeadlineFont = document.fonts.check(`1em "${headlineFont}"`) ? headlineFont : FONT_HEADLINE_FALLBACK;
const effectiveBodyFont = document.fonts.check(`1em "${bodyFont}"`) ? bodyFont : FONT_BODY_FALLBACK;
// 2. Canvas Setup
const canvas = document.createElement('canvas');
canvas.width = posterWidth;
canvas.height = posterHeight;
const ctx = canvas.getContext('2d');
// 3. Draw Background & Optional Noise
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, posterWidth, posterHeight);
if (applyNoiseToBackground === 1 && noiseIntensity > 0) {
const imageData = ctx.getImageData(0, 0, posterWidth, posterHeight);
const pixels = imageData.data;
const len = pixels.length;
const noiseFactor = 255 * Math.max(0, Math.min(1, noiseIntensity)); // Clamp intensity
for (let i = 0; i < len; i += 4) {
// Add monochrome noise to RGB channels
const noiseVal = (Math.random() - 0.5) * noiseFactor;
pixels[i] = Math.max(0, Math.min(255, pixels[i] + noiseVal));
pixels[i + 1] = Math.max(0, Math.min(255, pixels[i + 1] + noiseVal));
pixels[i + 2] = Math.max(0, Math.min(255, pixels[i + 2] + noiseVal));
// Alpha channel (pixels[i+3]) remains unchanged
}
ctx.putImageData(imageData, 0, 0);
}
// 4. Layout Constants
const M = posterWidth * 0.05; // Margin
const CW = posterWidth - 2 * M; // Content width for text elements
ctx.fillStyle = textColor; // Set default text color
ctx.textBaseline = "top"; // Consistent baseline for text Y positioning
// 5. "WANTED" Text
ctx.textAlign = "center"; // Center all text horizontally
const wantedTextSize = posterHeight * 0.12;
ctx.font = `${wantedTextSize}px "${effectiveHeadlineFont}"`;
const yWanted = M;
ctx.fillText(wantedText.toUpperCase(), posterWidth / 2, yWanted);
// 6. "SUBTEXT" (e.g., DEAD OR ALIVE)
const subTextSize = posterHeight * 0.04;
ctx.font = `${subTextSize}px "${effectiveBodyFont}"`;
const ySubtext = yWanted + wantedTextSize * 0.9 + (M * 0.1); // Positioned below "WANTED"
ctx.fillText(subText.toUpperCase(), posterWidth / 2, ySubtext);
// 7. Image Area Setup
const imgAreaTop = ySubtext + subTextSize + M;
const imgAreaHeight = posterHeight * 0.40; // Image gets 40% of poster height
const maxImgDisplayWidth = CW * 0.9; // Max width for image display (90% of content width)
const maxImgDisplayHeight = imgAreaHeight;
let imgDrawWidth = 0;
let imgDrawHeight = 0;
let imageToDraw = originalImg; // Default to original image
if (originalImg && originalImg.naturalWidth > 0 && originalImg.naturalHeight > 0) {
imgDrawWidth = originalImg.naturalWidth;
imgDrawHeight = originalImg.naturalHeight;
const imgAspectRatio = imgDrawWidth / imgDrawHeight;
// Scale image to fit within maxImgDisplayWidth and maxImgDisplayHeight
if (imgDrawWidth > maxImgDisplayWidth) {
imgDrawWidth = maxImgDisplayWidth;
imgDrawHeight = imgDrawWidth / imgAspectRatio;
}
if (imgDrawHeight > maxImgDisplayHeight) {
imgDrawHeight = maxImgDisplayHeight;
imgDrawWidth = imgDrawHeight * imgAspectRatio;
}
// Final check for width constraint after height adjustment
if (imgDrawWidth > maxImgDisplayWidth) {
imgDrawWidth = maxImgDisplayWidth;
imgDrawHeight = imgDrawWidth / imgAspectRatio;
}
// Apply Sepia filter to Image if requested
if (applySepiaToImage === 1) {
const tempCanvas = document.createElement('canvas');
tempCanvas.width = imgDrawWidth;
tempCanvas.height = imgDrawHeight;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(originalImg, 0, 0, imgDrawWidth, imgDrawHeight); // Draw scaled image to temp
const imageData = tempCtx.getImageData(0, 0, imgDrawWidth, imgDrawHeight);
const pixels = imageData.data;
for (let i = 0; i < pixels.length; i += 4) {
const r = pixels[i], g = pixels[i+1], b = pixels[i+2];
pixels[i] = Math.min(255, r * 0.393 + g * 0.769 + b * 0.189); // Red
pixels[i+1] = Math.min(255, r * 0.349 + g * 0.686 + b * 0.168); // Green
pixels[i+2] = Math.min(255, r * 0.272 + g * 0.534 + b * 0.131); // Blue
}
tempCtx.putImageData(imageData, 0, 0);
imageToDraw = tempCanvas; // Update imageToDraw to the sepia version
}
}
// Calculate image position (centered in its allocated space)
const imgX = (posterWidth - imgDrawWidth) / 2;
const imgY = imgAreaTop + (maxImgDisplayHeight - imgDrawHeight) / 2;
// Draw the image (original or sepia-toned, or placeholder if invalid)
if (imgDrawWidth > 0 && imgDrawHeight > 0) {
ctx.drawImage(imageToDraw, imgX, imgY, imgDrawWidth, imgDrawHeight);
// Draw Image Border
if (imageBorderWidth > 0) {
ctx.strokeStyle = imageBorderColor;
ctx.lineWidth = imageBorderWidth;
// Border is drawn centered on the image edge, so half inside, half outside
ctx.strokeRect(imgX, imgY, imgDrawWidth, imgDrawHeight);
}
} else { // Placeholder if image is not available/valid
const phX = (posterWidth - maxImgDisplayWidth * 0.8) / 2;
const phY = imgAreaTop + (maxImgDisplayHeight - maxImgDisplayHeight * 0.8) / 2;
const phW = maxImgDisplayWidth * 0.8;
const phH = maxImgDisplayHeight * 0.8;
ctx.strokeStyle = textColor;
ctx.lineWidth = 1;
ctx.strokeRect(phX, phY, phW, phH);
ctx.font = `${posterHeight * 0.02}px "${effectiveBodyFont}"`;
ctx.textAlign = "center";
// Vertical center for placeholder text (simple approximation)
ctx.fillText("Image Not Available", phX + phW / 2, phY + phH / 2 - (posterHeight * 0.02) / 2);
}
// 8. Bounty Name
// Y position calculations are based on the allocated image area, not the actual image dimensions
// This ensures text placement is consistent even if image is missing/small.
let yCurrent = imgAreaTop + maxImgDisplayHeight + M;
if (imgDrawWidth > 0 && imageBorderWidth > 0) { // Add half border width if border exists
yCurrent += imageBorderWidth / 2;
}
const nameTextSize = Math.min(posterHeight * 0.07, CW / (bountyName.length * 0.45 + 1)); // Auto-size font
ctx.font = `${nameTextSize}px "${effectiveBodyFont}"`;
ctx.fillText(bountyName.toUpperCase(), posterWidth / 2, yCurrent);
yCurrent += nameTextSize + (M * 0.4);
// 9. Reward Amount
const rewardTextSize = Math.min(posterHeight * 0.05, CW / (rewardAmount.length * 0.4 + 1)); // Auto-size font
ctx.font = `${rewardTextSize}px "${effectiveBodyFont}"`;
ctx.fillText(rewardAmount.toUpperCase(), posterWidth / 2, yCurrent);
yCurrent += rewardTextSize + M;
// 10. Crime Description
const descTextSize = posterHeight * 0.025;
const crimeDescMaxWidth = CW * 0.9; // Description can take up 90% of content width
ctx.fillStyle = textColor; // Ensure text color set for wrapText
_wrapText(ctx, crimeDescription, posterWidth / 2, yCurrent, crimeDescMaxWidth, descTextSize, bodyFont, FONT_BODY_FALLBACK);
return canvas;
}
Apply Changes