You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
nameStr = "VAULT DWELLER",
crimeStr = "LOOTING & GENERAL MAYHEM",
rewardStr = "500 CAPS",
posterFont = "Metal Mania",
paperColorStr = "210,180,140", // RGB string for paper, e.g., "210,180,140" for tan
textColorStr = "56,34,15", // RGB string for main text, e.g., "56,34,15" for dark brown
wantedTextColorStr = "139,0,0" // RGB string for "WANTED" text, e.g., "139,0,0" for dark red
) {
const FONT_MAP = {
"Metal Mania": "https://fonts.gstatic.com/s/metalmania/v20/ocrP2dEJ5_AIOsumS_cR_Y5KM_21.woff2",
"Special Elite": "https://fonts.gstatic.com/s/specialelite/v17/X7nP4b8XQvSyT2S9SY9nPEhFVvo1WkY.woff2",
"Bangers": "https://fonts.gstatic.com/s/bangers/v20/FeVQS0BTqb0h60ACL5k.woff2",
"Creepster": "https://fonts.gstatic.com/s/creepster/v15/AlZy_zVVcp9OKAbP_rpalw.woff2",
"Impact": null, // System font
"Arial": null // System font
};
async function loadCustomFont(fontFamily, fontUrl) {
if (!fontUrl) return true; // System font, assumed available
if (document.fonts) {
// Check if already loaded by the browser or a previous call
if (await document.fonts.check(`12px "${fontFamily}"`)) {
return true;
}
const fontFace = new FontFace(fontFamily, `url(${fontUrl}) format('woff2')`);
try {
await fontFace.load();
document.fonts.add(fontFace);
// Verify it's truly usable
if (!await document.fonts.check(`12px "${fontFamily}"`)) {
console.warn(`Font ${fontFamily} loaded but check failed.`);
// Fallback or error if critical? For now, continue, browser might still use it.
}
return true;
} catch (e) {
console.error(`Font ${fontFamily} from ${fontUrl} failed to load:`, e);
return false;
}
} else {
console.warn("document.fonts API not supported. Cannot load custom fonts.");
return false; // Cannot load custom font
}
}
let actualFontFamily = posterFont;
const fontUrl = FONT_MAP[posterFont];
if (fontUrl !== undefined) { // Known font
const fontLoaded = await loadCustomFont(posterFont, fontUrl);
if (!fontLoaded && fontUrl) { // Custom font failed to load
console.warn(`Falling back to system font 'Arial' for ${posterFont}.`);
actualFontFamily = "Arial"; // Fallback font
}
} else { // Unknown font, assume it's a system font or user knows what they're doing
actualFontFamily = posterFont;
}
// Parse RGB color strings
const parseRgb = (rgbStr, defaultColor = "0,0,0") => {
try {
const parts = rgbStr.split(',').map(s => parseInt(s.trim(), 10));
if (parts.length === 3 && parts.every(p => !isNaN(p) && p >= 0 && p <= 255)) {
return `rgb(${parts[0]},${parts[1]},${parts[2]})`;
}
} catch (e) { /* fall through to default */ }
const defaultParts = defaultColor.split(',').map(s => parseInt(s.trim(), 10));
return `rgb(${defaultParts[0]},${defaultParts[1]},${defaultParts[2]})`;
};
const parsedPaperColor = parseRgb(paperColorStr, "210,180,140");
const parsedTextColor = parseRgb(textColorStr, "56,34,15");
const parsedWantedTextColor = parseRgb(wantedTextColorStr, "139,0,0");
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const POSTER_WIDTH = 600;
const POSTER_HEIGHT = 900;
const MARGIN = 30;
canvas.width = POSTER_WIDTH;
canvas.height = POSTER_HEIGHT;
// 1. Background
ctx.fillStyle = parsedPaperColor;
ctx.fillRect(0, 0, POSTER_WIDTH, POSTER_HEIGHT);
// Add noise to background
function addNoise(context, x, y, width, height, amount = 20) {
const imageData = context.getImageData(x, y, width, height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const noise = (Math.random() - 0.5) * amount;
data[i] = Math.max(0, Math.min(255, data[i] + noise));
data[i+1] = Math.max(0, Math.min(255, data[i+1] + noise));
data[i+2] = Math.max(0, Math.min(255, data[i+2] + noise));
}
context.putImageData(imageData, x, y);
}
addNoise(ctx, 0, 0, POSTER_WIDTH, POSTER_HEIGHT, 25);
// Add stains
function addStain(context, centerX, centerY, maxRadius, colorStr) {
const [r,g,b] = colorStr.split(',').map(Number);
context.fillStyle = `rgba(${r},${g},${b}, ${0.05 + Math.random() * 0.1})`; // Random opacity
const segments = 10 + Math.floor(Math.random() * 10);
context.beginPath();
const startRadius = maxRadius * (0.7 + Math.random() * 0.3);
context.moveTo(centerX + startRadius * Math.cos(0), centerY + startRadius * Math.sin(0));
for (let i = 1; i <= segments; i++) {
const angle = (i / segments) * 2 * Math.PI;
const radius = maxRadius * (0.5 + Math.random() * 0.5); // Irregular radius
context.lineTo(centerX + Math.cos(angle) * radius, centerY + Math.sin(angle) * radius);
}
context.closePath();
context.fill();
}
const stainBaseColor = textColorStr.split(',').map(n => Math.max(0, parseInt(n) - 20)).join(','); // Slightly lighter than text
for(let i=0; i < 5; i++) { // Add 5 random stains
addStain(ctx, Math.random() * POSTER_WIDTH, Math.random() * POSTER_HEIGHT, Math.random() * 50 + 50, stainBaseColor);
}
// 2. Main Poster Border
ctx.strokeStyle = parsedTextColor;
ctx.lineWidth = MARGIN / 2; // 15px border
ctx.strokeRect(MARGIN/4, MARGIN/4, POSTER_WIDTH - MARGIN/2, POSTER_HEIGHT - MARGIN/2);
ctx.lineWidth = 2; // Reset for other strokes
let currentY = MARGIN * 1.5; // Start Y position
// 3. "WANTED" Text
ctx.font = `bold 90px "${actualFontFamily}"`;
ctx.fillStyle = parsedWantedTextColor;
ctx.textAlign = 'center';
currentY += 70;
ctx.fillText("WANTED", POSTER_WIDTH / 2, currentY);
// 4. "DEAD OR ALIVE" (or similar)
currentY += 40;
ctx.font = `30px "${actualFontFamily}"`;
ctx.fillStyle = parsedTextColor; // Regular text color
ctx.fillText("DEAD OR ALIVE (PREFERABLY DEAD)", POSTER_WIDTH / 2, currentY);
currentY += 30; // Space before image
// 5. Process and Draw Image
const imgContainerX = MARGIN;
const imgContainerY = currentY;
const imgContainerWidth = POSTER_WIDTH - 2 * MARGIN;
const imgContainerHeight = 300; // Max height for image
// Create a temporary canvas for image processing
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = originalImg.naturalWidth;
tempCanvas.height = originalImg.naturalHeight;
tempCtx.drawImage(originalImg, 0, 0);
// Grayscale and Contrast filter
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
const data = imageData.data;
const contrast = 1.8; // Adjusted contrast
const brightness = -10; // Slight darkening
for (let i = 0; i < data.length; i += 4) {
let r = data[i], g = data[i+1], b = data[i+2];
// Grayscale (luminosity)
const avg = 0.299 * r + 0.587 * g + 0.114 * b;
r = g = b = avg;
// Apply brightness
r += brightness; g += brightness; b += brightness;
// Apply contrast
const factor = (259 * (contrast * 255 + 255)) / (255 * (259 - contrast * 255)); // standard formula
r = factor * (r - 128) + 128;
g = factor * (g - 128) + 128;
b = factor * (b - 128) + 128;
data[i] = Math.max(0, Math.min(255, r));
data[i+1] = Math.max(0, Math.min(255, g));
data[i+2] = Math.max(0, Math.min(255, b));
}
tempCtx.putImageData(imageData, 0, 0);
addNoise(tempCtx, 0, 0, tempCanvas.width, tempCanvas.height, 35); // Add grain to image
// Calculate drawing dimensions for the image to fit and be centered
let drawWidth, drawHeight;
const imgAspect = tempCanvas.width / tempCanvas.height;
if ((imgContainerWidth / imgContainerHeight) > imgAspect) { // Container is wider aspect than image
drawHeight = imgContainerHeight;
drawWidth = drawHeight * imgAspect;
} else { // Container is taller/squarer aspect than image
drawWidth = imgContainerWidth;
drawHeight = drawWidth / imgAspect;
}
const drawX = imgContainerX + (imgContainerWidth - drawWidth) / 2;
const drawY = imgContainerY;
ctx.drawImage(tempCanvas, drawX, drawY, drawWidth, drawHeight);
// Border around the image
ctx.strokeStyle = parsedTextColor;
ctx.lineWidth = 4;
ctx.strokeRect(drawX-2, drawY-2, drawWidth+4, drawHeight+4); // Slightly outset border
currentY += imgContainerHeight + 20; // Update currentY past the image block
// Helper for text wrapping
function wrapText(context, text, x, y, maxWidth, lineHeight, fontStyle, fillStyle) {
context.font = fontStyle;
context.fillStyle = fillStyle;
context.textAlign = 'center';
const words = text.toUpperCase().split(' ');
let line = '';
let currentTextY = y;
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, currentTextY);
line = words[n] + ' ';
currentTextY += lineHeight;
} else {
line = testLine;
}
}
context.fillText(line.trim(), x, currentTextY);
return currentTextY + lineHeight; // Return Y position after the last line of text
}
// 6. Name Text
currentY += 40;
ctx.font = `bold 50px "${actualFontFamily}"`;
ctx.fillStyle = parsedTextColor;
wrapText(ctx, nameStr, POSTER_WIDTH / 2, currentY, POSTER_WIDTH - 2 * MARGIN, 50, `bold 50px "${actualFontFamily}"`, parsedTextColor);
// Estimate height of name text - assume one line mostly for simplicity here, adjust if wrapText changes currentY
currentY += ctx.measureText(nameStr).actualBoundingBoxDescent || 50;
// 7. Crime Text
currentY += 30;
ctx.font = `25px "${actualFontFamily}"`;
ctx.fillText("KNOWN CRIMES:", POSTER_WIDTH / 2, currentY);
currentY += 35;
const crimeLineHeight = 30;
currentY = wrapText(ctx, crimeStr, POSTER_WIDTH / 2, currentY, POSTER_WIDTH - 2 * MARGIN - 40, crimeLineHeight, `22px "${actualFontFamily}"`, parsedTextColor);
// currentY is already updated by wrapText
// 8. Reward Text
currentY += 20; // Space before reward
ctx.font = `bold 30px "${actualFontFamily}"`;
ctx.fillStyle = parsedTextColor;
ctx.fillText("REWARD", POSTER_WIDTH / 2, currentY);
currentY += 50;
ctx.font = `bold 45px "${actualFontFamily}"`;
ctx.fillStyle = parsedWantedTextColor; // Use "wanted" color for reward amount
ctx.fillText(rewardStr.toUpperCase(), POSTER_WIDTH / 2, currentY);
// Final touch: ensure bottom margin
// if (currentY > POSTER_HEIGHT - MARGIN) { ... canvas might need to be taller dynamically ... }
// For fixed size, this is it.
return canvas;
}
Apply Changes