You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
title = "THE SHADOW CIPHER",
tagline = "In a city of secrets, truth is the deadliest weapon.",
starring = "NICK NOIR, VERONICA VALE, EDDIE 'THE FIXER' MALONE",
directorText = "J. HUSTON JR.",
posterWidth = 800,
posterHeight = 1200,
titleColor = "white",
taglineColor = "#e0e0e0",
starringColor = "white",
directorColor = "#c0c0c0",
backgroundColor = "#050505",
contrast = 1.9,
grainAmount = 25, // 0 for no grain, higher for more visible grain
vignetteIntensity = 0.65 // 0 for no vignette, 1 for very strong
) {
const FONT_NAME = 'Bebas Neue';
let fontLoaded = false;
// Helper to check if font is loaded via CSS Font Loading API
const checkFontLoaded = async () => {
try {
// Check for at least one character in the font
return await document.fonts.load(`1em "${FONT_NAME}"`);
} catch (e) {
// Error (e.g., API not supported, though unlikely in modern browsers)
console.warn("Font loading API check failed:", e);
return false;
}
};
// Try to load font if already linked or system font
if (await checkFontLoaded()) {
fontLoaded = true;
}
// If not loaded, dynamically add the font link
if (!fontLoaded && !document.querySelector(`link[href*="${FONT_NAME.replace(' ', '+')}"]`)) {
const link = document.createElement('link');
link.href = `https://fonts.googleapis.com/css2?family=${FONT_NAME.replace(' ', '+')}:wght@400&display=swap`;
link.rel = 'stylesheet';
const promiseFontLinkLoad = new Promise((resolve) => {
link.onload = () => resolve(true);
link.onerror = () => resolve(false); // Stylesheet failed to load
});
document.head.appendChild(link);
if (await promiseFontLinkLoad) {
// After stylesheet loads, try to confirm with Font Loading API
if (await checkFontLoaded()) {
fontLoaded = true;
} else {
// Fallback: Assume font might be usable after a short delay if API check fails post-load
await new Promise(resolve => setTimeout(resolve, 300));
if (await checkFontLoaded()) fontLoaded = true;
}
} else {
console.warn(`Failed to load stylesheet for font ${FONT_NAME}.`);
}
}
if (!fontLoaded) {
console.warn(`Font ${FONT_NAME} not available or not confirmed loaded. Using fallbacks.`);
}
const canvas = document.createElement('canvas');
canvas.width = posterWidth;
canvas.height = posterHeight;
const ctx = canvas.getContext('2d');
// Fill background (acts as an underlay if image has transparency or doesn't fully cover)
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Create an offscreen canvas for image manipulation
const imgCanvas = document.createElement('canvas');
imgCanvas.width = originalImg.naturalWidth; // Use natural dimensions for full quality processing
imgCanvas.height = originalImg.naturalHeight;
const imgCtx = imgCanvas.getContext('2d', { willReadFrequently: true }); // Hint for performance
// Draw original image to offscreen canvas
imgCtx.drawImage(originalImg, 0, 0);
// Apply Grayscale & Contrast
const imageData = imgCtx.getImageData(0, 0, imgCanvas.width, imgCanvas.height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
let gray = 0.299 * r + 0.587 * g + 0.114 * b; // Standard luminance calculation
// Apply contrast
gray = contrast * (gray - 128) + 128;
gray = Math.max(0, Math.min(255, gray)); // Clamp to 0-255 range
data[i] = gray; // Red
data[i + 1] = gray; // Green
data[i + 2] = gray; // Blue
}
imgCtx.putImageData(imageData, 0, 0);
// Draw processed image onto main canvas, scaled to cover
const canvasAspect = canvas.width / canvas.height;
const processedImgAspect = imgCanvas.width / imgCanvas.height;
let sx = 0, sy = 0, sWidth = imgCanvas.width, sHeight = imgCanvas.height;
if (processedImgAspect > canvasAspect) { // Image is wider than canvas target aspect
sWidth = imgCanvas.height * canvasAspect;
sx = (imgCanvas.width - sWidth) / 2;
} else { // Image is taller or same aspect
sHeight = imgCanvas.width / canvasAspect;
sy = (imgCanvas.height - sHeight) / 2;
}
ctx.drawImage(imgCanvas, sx, sy, sWidth, sHeight, 0, 0, canvas.width, canvas.height);
// Apply vignette (optional)
if (vignetteIntensity > 0) {
const outerRadius = Math.sqrt(canvas.width * canvas.width + canvas.height * canvas.height) / 2; // Diagonal half
const gradient = ctx.createRadialGradient(
canvas.width / 2, canvas.height / 2, canvas.height / 3.5, // Inner circle
canvas.width / 2, canvas.height / 2, outerRadius * 0.85 // Outer circle (adjust factor for spread)
);
gradient.addColorStop(0, `rgba(0,0,0,0)`);
gradient.addColorStop(1, `rgba(0,0,0,${Math.min(1, vignetteIntensity)})`); // Intensity capped at 1
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
// Apply Film Grain (optional, applied to background before text)
if (grainAmount > 0) {
const bgImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const bgData = bgImageData.data;
for (let i = 0; i < bgData.length; i += 4) {
const noise = (Math.random() - 0.5) * grainAmount;
bgData[i] = Math.max(0, Math.min(255, bgData[i] + noise));
bgData[i + 1] = Math.max(0, Math.min(255, bgData[i + 1] + noise));
bgData[i + 2] = Math.max(0, Math.min(255, bgData[i + 2] + noise));
}
ctx.putImageData(bgImageData, 0, 0);
}
// Text rendering setup
const FONT_PRIMARY = fontLoaded ? `"${FONT_NAME}"` : "Impact";
const FONT_FALLBACK = "Arial, 'Helvetica Neue', Helvetica, sans-serif";
ctx.textAlign = 'center';
ctx.textBaseline = 'middle'; // Vertically align text to its center
// TITLE
const titleSize = Math.floor(canvas.width / 8); // Responsive font size
ctx.font = `bold ${titleSize}px ${FONT_PRIMARY}, ${FONT_FALLBACK}`; // Add fallback
ctx.fillStyle = titleColor;
ctx.shadowColor = 'rgba(0, 0, 0, 0.9)';
ctx.shadowBlur = 12;
ctx.shadowOffsetX = 4;
ctx.shadowOffsetY = 4;
const titleWords = title.toUpperCase().split(' ');
let currentLine = '';
const titleLines = [];
const maxTitleWidth = canvas.width * 0.9; // Max width for a title line
for (let n = 0; n < titleWords.length; n++) {
const testLine = currentLine + titleWords[n] + ' ';
if (ctx.measureText(testLine.trim()).width > maxTitleWidth && n > 0 && currentLine) {
titleLines.push(currentLine.trim());
currentLine = titleWords[n] + ' ';
} else {
currentLine = testLine;
}
}
if (currentLine.trim()) titleLines.push(currentLine.trim());
const lineHeightTitle = titleSize * 0.9;
const totalTitleHeightEst = titleLines.length * lineHeightTitle;
let titleBlockStartY = (canvas.height * 0.22) - (totalTitleHeightEst / 2); // Center block around 22% from top
if (titleLines.length === 1) titleBlockStartY = canvas.height * 0.20;
else if (titleLines.length > 2) titleBlockStartY = canvas.height * 0.18; // Move up if more lines
titleLines.forEach((line, index) => {
// Add half line height because textBaseline is middle
ctx.fillText(line, canvas.width / 2, titleBlockStartY + (index * lineHeightTitle) + lineHeightTitle / 2);
});
const titleBlockEndY = titleBlockStartY + totalTitleHeightEst;
// TAGLINE
const taglineSize = Math.floor(canvas.width / 32);
ctx.font = `italic ${taglineSize}px ${FONT_FALLBACK}`; // Use a more standard font for tagline
ctx.fillStyle = taglineColor;
ctx.shadowColor = 'rgba(0,0,0,0.7)';
ctx.shadowBlur = 6;
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
const taglineText = tagline.toUpperCase();
const maxTaglineWidth = canvas.width * 0.7;
const taglineWords = taglineText.split(' ');
let taglineLine = "";
const taglineLines = [];
for(let i=0; i<taglineWords.length; i++){
let testLine = taglineLine + taglineWords[i] + " ";
if(ctx.measureText(testLine.trim()).width > maxTaglineWidth && i > 0 && taglineLine){
taglineLines.push(taglineLine.trim());
taglineLine = taglineWords[i] + " ";
} else {
taglineLine = testLine;
}
}
if (taglineLine.trim()) taglineLines.push(taglineLine.trim());
let currentTaglineY = titleBlockEndY + taglineSize * 1.5; // Start below title
taglineLines.forEach((line, index) => {
ctx.fillText(line, canvas.width / 2, currentTaglineY + index * taglineSize * 1.1 + taglineSize * 0.5);
});
ctx.shadowColor = 'transparent'; // Reset shadow for subsequent text elements
// STARRING
let currentY = canvas.height * 0.72;
const starringHeaderSize = Math.floor(canvas.width / 26);
ctx.font = `bold ${starringHeaderSize}px ${FONT_PRIMARY}, ${FONT_FALLBACK}`;
ctx.fillStyle = starringColor;
ctx.shadowColor = 'rgba(0, 0, 0, 0.6)'; ctx.shadowBlur = 4; ctx.shadowOffsetX = 1; ctx.shadowOffsetY = 1;
ctx.fillText("STARRING", canvas.width / 2, currentY + starringHeaderSize / 2);
currentY += starringHeaderSize * 1.3; // Space after "STARRING"
const starsArray = starring.toUpperCase().split(',').map(s => s.trim()).filter(s => s.length > 0);
const starringNameSize = Math.floor(canvas.width / 30);
ctx.font = `${starringNameSize}px ${FONT_PRIMARY}, ${FONT_FALLBACK}`;
ctx.fillStyle = starringColor;
const maxStarringWidth = canvas.width * 0.85;
const starLineTexts = [];
let currentStarLine = "";
starsArray.forEach((name) => {
let prospectiveLine = currentStarLine ? currentStarLine + ", " + name : name;
if (ctx.measureText(prospectiveLine).width > maxStarringWidth && currentStarLine) {
starLineTexts.push(currentStarLine);
currentStarLine = name;
} else {
currentStarLine = prospectiveLine;
}
});
if (currentStarLine) starLineTexts.push(currentStarLine);
starLineTexts.forEach((line, index) => {
ctx.fillText(line, canvas.width / 2, currentY + index * starringNameSize * 1.1 + starringNameSize / 2);
});
ctx.shadowColor = 'transparent';
// DIRECTOR
const directorY = canvas.height * 0.96; // Near bottom
const directorSize = Math.floor(canvas.width / 45);
ctx.font = `${directorSize}px ${FONT_FALLBACK}`;
ctx.fillStyle = directorColor;
ctx.fillText("Directed by " + directorText.toUpperCase(), canvas.width / 2, directorY + directorSize / 2);
return canvas;
}
Apply Changes