You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, title = "THE CONJURING 2", topText = "THE NEXT TRUE STORY FROM THE CASE FILES OF ED AND LORRAINE WARREN", bottomText = "BASED ON TRUE EVENTS") {
// Initialize Canvas
const canvas = document.createElement('canvas');
const width = originalImg.width;
const height = originalImg.height;
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
// Dynamically Inject and Load "Cinzel" Google Font for Cinematic Typography
const fontName = 'Cinzel';
const fontTagId = 'cinzel-font-tag';
if (!document.getElementById(fontTagId)) {
const link = document.createElement('link');
link.id = fontTagId;
link.href = 'https://fonts.googleapis.com/css2?family=Cinzel:wght@500;700&display=swap';
link.rel = 'stylesheet';
document.head.appendChild(link);
}
try {
await document.fonts.load(`700 24px "${fontName}"`);
await document.fonts.load(`500 24px "${fontName}"`);
} catch (e) {
// Fallback to standard serif gracefully if it gets blocked
console.warn("Could not load Google Font. Using fallback 'serif'.");
}
// Fill black and Draw Original Image (safeguard for transparent images like PNGs)
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, width, height);
ctx.drawImage(originalImg, 0, 0);
// Precompute a pseudo-random Noise Table (optimizing the hot loop by avoiding Math.random())
const noiseTable = new Float32Array(4096);
for (let i = 0; i < 4096; i++) {
noiseTable[i] = (Math.random() - 0.5) * 15;
}
// Color Grading: High Contrast & Cyan/Teal Shadows (Cinematic Horror Map)
const imgData = ctx.getImageData(0, 0, width, height);
const data = imgData.data;
const cx = width / 2;
const cy = height / 2;
const maxDist = Math.sqrt((width / 2) * (width / 2) + (height / 2) * (height / 2));
const contrast = 1.25;
const gradeAmount = 0.75;
for (let i = 0, len = data.length; i < len; i += 4) {
let r = data[i];
let g = data[i + 1];
let b = data[i + 2];
// 1. Boosted Contrast
r = ((r / 255 - 0.5) * contrast + 0.5) * 255;
g = ((g / 255 - 0.5) * contrast + 0.5) * 255;
b = ((b / 255 - 0.5) * contrast + 0.5) * 255;
// Calculate Luminance
const lum = 0.299 * r + 0.587 * g + 0.114 * b;
// 2. Map to eerie cyan/blue 3-stop gradient
let tr, tg, tb;
if (lum < 85) { // Shadows
const t = lum / 85;
tr = 2 + 25 * t;
tg = 8 + 45 * t;
tb = 15 + 60 * t;
} else if (lum < 170) { // Midtones
const t = (lum - 85) / 85;
tr = 27 + 60 * t;
tg = 53 + 70 * t;
tb = 75 + 75 * t;
} else { // Highlights
const t = (lum - 170) / 85;
tr = 87 + 158 * t;
tg = 123 + 127 * t;
tb = 150 + 105 * t;
}
// 3. Blend cinematic grade back into contrast-boosted original
r = r * (1 - gradeAmount) + tr * gradeAmount;
g = g * (1 - gradeAmount) + tg * gradeAmount;
b = b * (1 - gradeAmount) + tb * gradeAmount;
// 4. Eerie Vignette (Slightly raised center for character portraits)
const pxl = i >> 2;
const x = pxl % width;
const y = Math.floor(pxl / width);
const dx = x - cx;
const dy = y - (cy * 0.9);
const dist = Math.sqrt(dx * dx + dy * dy); // Usually faster than Math.hypot in JS
let vignette = 1 - (dist / maxDist);
if (vignette < 0) vignette = 0;
vignette = Math.pow(vignette, 0.7);
vignette = 0.15 + 0.85 * vignette; // Soften extreme edge shadows slightly
// 5. Apply Film Grain
const noise = noiseTable[(x * 17 + y * 53) & 4095] * vignette;
// Assign back (values auto-clamp within Uint8ClampedArray bounds internally)
data[i] = (r * vignette) + noise;
data[i + 1] = (g * vignette) + noise;
data[i + 2] = (b * vignette) + noise;
}
ctx.putImageData(imgData, 0, 0);
// ----------------------------------------------------- //
// Typography System (Spacing and Fitting Engine)
// ----------------------------------------------------- //
function getSpacedWidth(context, txt, size, weight, spacingRatio) {
if (!txt) return 0;
context.font = `${weight} ${size}px "${fontName}", serif`;
let w = 0;
for (let char of txt) w += context.measureText(char).width;
return w + (size * spacingRatio * (txt.length - 1));
}
function fillTextSpaced(context, txt, x, y, letterSpacing) {
if (!txt) return;
const chars = txt.split('');
let totalWidth = 0;
const widths = new Float32Array(chars.length);
for (let i = 0; i < chars.length; i++) {
const w = context.measureText(chars[i]).width;
widths[i] = w;
totalWidth += w;
}
totalWidth += letterSpacing * (chars.length - 1);
let currentX = x - totalWidth / 2;
for (let i = 0; i < chars.length; i++) {
context.fillText(chars[i], currentX, y);
currentX += widths[i] + letterSpacing;
}
}
ctx.shadowColor = 'rgba(0, 0, 0, 0.9)';
ctx.shadowBlur = Math.max(width, height) * 0.01;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = Math.max(width, height) * 0.003;
ctx.textAlign = 'left';
ctx.textBaseline = 'middle';
// Calculate dynamic scaling constraints for multi-tier text
let titleSize = width * 0.09;
let titleWidth = getSpacedWidth(ctx, title, titleSize, '700', 0.15);
if (titleWidth > width * 0.9 && titleWidth > 0) {
titleSize *= (width * 0.9) / titleWidth; // proportional scaling protects from freezing while-loops
}
let topTextSize = width * 0.0225;
let topWidth = getSpacedWidth(ctx, topText, topTextSize, '500', 0.2);
if (topWidth > width * 0.9 && topWidth > 0) {
topTextSize *= (width * 0.9) / topWidth;
}
let bottomTextSize = width * 0.0315;
let botWidth = getSpacedWidth(ctx, bottomText, bottomTextSize, '500', 0.25);
if (botWidth > width * 0.85 && botWidth > 0) {
bottomTextSize *= (width * 0.85) / botWidth;
}
// Render layers
if (topText) {
ctx.font = `500 ${topTextSize}px "${fontName}", serif`;
ctx.fillStyle = 'rgba(210, 220, 230, 0.85)'; // Bleak blue/gray
fillTextSpaced(ctx, topText.toUpperCase(), width / 2, height * 0.72, topTextSize * 0.2);
}
if (title) {
ctx.font = `700 ${titleSize}px "${fontName}", serif`;
ctx.fillStyle = 'rgba(245, 250, 255, 0.95)'; // Pale chill white
fillTextSpaced(ctx, title.toUpperCase(), width / 2, height * 0.8, titleSize * 0.15);
}
if (bottomText) {
ctx.font = `500 ${bottomTextSize}px "${fontName}", serif`;
ctx.fillStyle = 'rgba(180, 195, 210, 0.9)';
fillTextSpaced(ctx, bottomText.toUpperCase(), width / 2, height * 0.88, bottomTextSize * 0.25);
}
return canvas;
}
Apply Changes