You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg,
titleText = "THE LAST ALGORITHM",
taglineText = "In a world of forgotten code, destiny will be compiled.",
starringText = "Starring: ANNA BYTES, GREG KERNEL",
additionalInfoText = "A PIXEL PERFECT PICTURES Release • Directed by CTRL+ALT+DEL",
creditsText = "COPYRIGHT © MMXXIV VINTAGE CODERS GUILD - ALL RIGHTS RESERVED",
fontFamilyTitleParam = "Bebas Neue", // Example: 'Bebas Neue', 'Anton', 'Impact'
fontFamilyBodyParam = "Oswald", // Example: 'Oswald', 'Roboto Condensed', 'Arial Narrow'
textColorParam = "rgba(250, 240, 220, 0.95)", // Off-white/ivory for general text
titleColorParam = "rgba(255, 215, 100, 1)", // Vintage gold/yellow for title
shadowColorParam = "rgba(0, 0, 0, 0.6)", // Shadow color for text
sepiaAmountParam = 0.75, // 0 to 1 for sepia intensity
contrastAmountParam = 1.15, // e.g., 1.0 is normal, 1.5 is 150% contrast
brightnessAmountParam = 0.95, // e.g., 1.0 is normal, 0.8 is 80% brightness
noiseAmountParam = 0.05 // 0 to 1 for noise/grain intensity (0 disables)
) {
// Helper function to load Google Fonts
async function _loadGoogleFontPosterHelper(fontFamilyName) {
const cleanedName = String(fontFamilyName).split(',')[0].replace(/'/g, '').trim();
if (!cleanedName) return; // No font name to load
// Request regular (400) and bold (700) weights if typically available
const fontQuery = cleanedName.replace(/ /g, '+') + ":wght@400;700";
const fontCssUrl = `https://fonts.googleapis.com/css2?family=${fontQuery}&display=swap`;
if (!document.querySelector(`link[href="${fontCssUrl}"]`)) {
const link = document.createElement('link');
link.href = fontCssUrl;
link.rel = 'stylesheet';
const p = new Promise((resolve) => {
link.onload = resolve;
link.onerror = () => {
console.warn(`Failed to load stylesheet for font: ${cleanedName} from ${fontCssUrl}`);
resolve(); // Resolve anyway to not block, will fallback to system fonts
};
});
document.head.appendChild(link);
await p;
}
try {
// Ensure browser has processed the font definitions for canvas use
await Promise.all([
document.fonts.load(`12px "${cleanedName}"`), // Test for regular weight
document.fonts.load(`bold 12px "${cleanedName}"`) // Test for bold weight
]);
} catch (e) {
console.warn(`Font ${cleanedName} (weights regular/bold) might not be fully ready for canvas:`, e);
// Fallback delay if document.fonts.load fails or raises an error
await new Promise(resolve => setTimeout(resolve, 300));
}
}
// Construct font stacks with fallbacks
const fontFamilyTitle = `'${fontFamilyTitleParam}', Impact, 'Arial Black', sans-serif`;
const fontFamilyBody = `'${fontFamilyBodyParam}', 'Arial Narrow', 'Roboto Condensed', sans-serif`;
// Load specified fonts
await Promise.all([
_loadGoogleFontPosterHelper(fontFamilyTitleParam),
_loadGoogleFontPosterHelper(fontFamilyBodyParam)
]);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const imgWidth = originalImg.naturalWidth;
const imgHeight = originalImg.naturalHeight;
// Define poster aspect ratio (standard movie one-sheet is 27x40 inches)
const posterAspectRatio = 27 / 40; // width / height
let canvasWidth, canvasHeight;
// Determine canvas size
const TARGET_POSTER_WIDTH = 800; // Target width for the poster in pixels
canvasWidth = TARGET_POSTER_WIDTH;
// Avoid excessive upscaling of the source image:
// If image width (scaled by maxUpscaleFactor) is less than target poster width, reduce poster width.
const maxUpscaleFactorImage = 2.0;
if (imgWidth > 0 && imgWidth * maxUpscaleFactorImage < canvasWidth) {
canvasWidth = imgWidth * maxUpscaleFactorImage;
}
// Similarly, if image height (scaled) implies an even smaller width due to aspect ratio, adjust further.
if (imgHeight > 0 && (imgHeight * maxUpscaleFactorImage) / posterAspectRatio < canvasWidth) {
canvasWidth = (imgHeight * maxUpscaleFactorImage) / posterAspectRatio * posterAspectRatio;
}
canvasWidth = Math.max(canvasWidth, 300); // Ensure a minimum poster width (e.g., 300px)
canvasHeight = canvasWidth / posterAspectRatio;
canvas.width = canvasWidth;
canvas.height = canvasHeight;
// Fill background (in case image doesn't cover all)
ctx.fillStyle = '#1a1a1a'; // Dark, slightly desaturated color
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
// Apply vintage image filters
ctx.filter = `sepia(${sepiaAmountParam}) contrast(${contrastAmountParam}) brightness(${brightnessAmountParam})`;
// Draw the image, scaled and cropped to fill the canvas (aspect fill/"cover")
const canvasAspect = canvasWidth / canvasHeight;
const imgAspect = imgWidth / imgHeight;
let sx = 0, sy = 0, sWidth = imgWidth, sHeight = imgHeight;
if (imgAspect > canvasAspect) { // Image is wider than canvas: fit height, crop width
sWidth = imgHeight * canvasAspect;
sx = (imgWidth - sWidth) / 2;
} else if (imgAspect < canvasAspect) { // Image is taller than canvas: fit width, crop height
sHeight = imgWidth / canvasAspect;
sy = (imgHeight - sHeight) / 2;
}
if (sWidth > 0 && sHeight > 0) { // Ensure non-zero source dimensions
ctx.drawImage(originalImg, sx, sy, sWidth, sHeight, 0, 0, canvasWidth, canvasHeight);
}
ctx.filter = 'none'; // Reset filter for text and other elements
// Add noise/grain overlay
if (noiseAmountParam > 0 && noiseAmountParam <= 1) {
const noiseCanvas = document.createElement('canvas');
noiseCanvas.width = canvasWidth;
noiseCanvas.height = canvasHeight;
const noiseCtx = noiseCanvas.getContext('2d');
const noisePixelData = noiseCtx.createImageData(canvasWidth, canvasHeight);
const d = noisePixelData.data;
const randomMax = 255 * noiseAmountParam;
for (let i = 0; i < d.length; i += 4) {
const grainValue = Math.floor(Math.random() * randomMax);
d[i] = grainValue;
d[i+1] = grainValue;
d[i+2] = grainValue;
// Alpha for noise spots: make them somewhat opaque. Experiment with this for desired effect.
// Subtle approach: low alpha overall for noise layer (see ctx.globalAlpha below)
// This sets individual pixel alpha for the noise pattern itself.
d[i+3] = Math.min(255, Math.floor(grainValue * 0.5 + 30));
}
noiseCtx.putImageData(noisePixelData, 0, 0);
ctx.globalAlpha = 0.30; // Adjust overall opacity of the noise layer
ctx.drawImage(noiseCanvas, 0, 0);
ctx.globalAlpha = 1.0; // Reset global alpha
}
// Text rendering setup
ctx.textAlign = 'center';
ctx.shadowColor = shadowColorParam;
// Scale shadow properties with canvas size for consistency
const baseShadowBlur = Math.max(3, canvasWidth * 0.005);
const baseShadowOffsetX = Math.max(1, canvasWidth * 0.002);
ctx.shadowBlur = baseShadowBlur;
ctx.shadowOffsetX = baseShadowOffsetX;
ctx.shadowOffsetY = baseShadowOffsetX; // Typically Y offset is same as X or slightly more
// Helper to draw wrapped text (uppercase for movie poster style)
function drawWrappedText(text, x, y, maxWidth, lineHeight, font, color, strokeColor = null, strokeWidth = 0) {
ctx.font = font;
ctx.fillStyle = color;
if (strokeColor && strokeWidth > 0) {
ctx.strokeStyle = strokeColor;
ctx.lineWidth = strokeWidth;
}
const words = String(text).toUpperCase().split(' ');
let line = '';
let currentY = y;
const linesToRender = [];
for (let n = 0; n < words.length; n++) {
const testLine = line + words[n] + ' ';
const metrics = ctx.measureText(testLine); // Measure uppercase text
if (metrics.width > maxWidth && n > 0) {
linesToRender.push({ text: line.trim(), y: currentY });
line = words[n] + ' ';
currentY += lineHeight;
} else {
line = testLine;
}
}
linesToRender.push({ text: line.trim(), y: currentY });
for (const lineObj of linesToRender) {
if (strokeColor && strokeWidth > 0) ctx.strokeText(lineObj.text, x, lineObj.y);
ctx.fillText(lineObj.text, x, lineObj.y);
}
return currentY + lineHeight; // Return Y position after this text block
}
// TITLE
const titleSize = Math.max(30, canvasHeight * 0.10); // Relative to poster height
let currentTextY = canvasHeight * 0.18; // Initial Y position for title block
currentTextY = drawWrappedText(
titleText,
canvasWidth / 2,
currentTextY,
canvasWidth * 0.90, // Max width for title
titleSize * 0.95, // Line height for title
`${titleSize}px ${fontFamilyTitle}`,
titleColorParam,
'rgba(0,0,0,0.8)', // Title stroke color
Math.max(1, titleSize * 0.025) // Title stroke width
);
// TAGLINE
const taglineSize = Math.max(14, canvasHeight * 0.03);
currentTextY += taglineSize * 0.3; // Add small space after title
currentTextY = drawWrappedText(
taglineText,
canvasWidth / 2,
currentTextY,
canvasWidth * 0.75,
taglineSize * 1.2,
`italic ${taglineSize}px ${fontFamilyBody}`, // Italic common for taglines
textColorParam
);
currentTextY += taglineSize * 1.5; // Add more space after tagline
// STARRING and ADDITIONAL INFO block
// Position this block starting around 78% down, or flow if title/tagline were very long
const creditsBlockYStart = canvasHeight * 0.78;
currentTextY = Math.max(currentTextY, creditsBlockYStart);
const starringSize = Math.max(12, canvasHeight * 0.028);
currentTextY = drawWrappedText(
starringText,
canvasWidth / 2,
currentTextY,
canvasWidth * 0.8,
starringSize * 1.3,
// Using 'bold' for starring if font supports it and it's loaded
`bold ${starringSize}px ${fontFamilyBody}`,
textColorParam
);
currentTextY += starringSize * 0.2; // Small space
const additionalInfoSize = Math.max(10, canvasHeight * 0.022);
currentTextY = drawWrappedText(
additionalInfoText,
canvasWidth / 2,
currentTextY,
canvasWidth * 0.85,
additionalInfoSize * 1.3,
`${additionalInfoSize}px ${fontFamilyBody}`,
textColorParam
);
// CREDITS (Billing Block) at the very bottom
const creditsSize = Math.max(8, canvasHeight * 0.018);
// Position relative to bottom edge; ensure it doesn't overlap significantly if content above is long
const creditsY = Math.max(currentTextY + creditsSize * 1.5, canvasHeight - creditsSize * 3);
if (currentTextY > (canvasHeight - creditsSize * 5) && creditsText) { // Heuristic: if dynamic text is already very low
console.warn("Text content may be pushing credits down or causing overlap. Consider shorter text inputs.");
}
drawWrappedText(
creditsText,
canvasWidth / 2,
creditsY,
canvasWidth * 0.9,
creditsSize * 1.15,
`${creditsSize}px ${fontFamilyBody}`,
textColorParam
);
// Reset shadow effects for any subsequent drawing (if canvas context were reused)
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
return canvas;
}
Apply Changes