You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
posterColorsStr = "#D72323,#222831,#EEEEEE,#F0A500", // Red, Dark Gray/Black, Off-White, Yellow/Gold
contrastFactor = 1.3,
posterizeLevels = 3,
noiseAmount = 0.05, // 0 to 1 for noise intensity, 0 to disable
addText = "",
textColor = "#FFFFFF",
textFont = "bold 40px 'Oswald', Impact, 'Arial Black', sans-serif",
textPositionXPercent = 0.5, // 0 to 1, percentage of width
textPositionYPercent = 0.9, // 0 to 1, percentage of height
textStrokeColor = "#000000",
textStrokeWidth = 3
) {
// --- Helper Functions ---
function hexToRgb(hex) {
const bigint = parseInt(hex.slice(1), 16);
const r = (bigint >> 16) & 255;
const g = (bigint >> 8) & 255;
const b = bigint & 255;
return { r, g, b };
}
function colorDistanceSq(color1, color2) {
const dr = color1.r - color2.r;
const dg = color1.g - color2.g;
const db = color1.b - color2.b;
return dr * dr + dg * dg + db * db;
}
function findClosestColor(pixelRgb, paletteRgbs) {
let closestColor = paletteRgbs[0];
let minDistanceSq = colorDistanceSq(pixelRgb, closestColor);
for (let i = 1; i < paletteRgbs.length; i++) {
const distanceSq = colorDistanceSq(pixelRgb, paletteRgbs[i]);
if (distanceSq < minDistanceSq) {
minDistanceSq = distanceSq;
closestColor = paletteRgbs[i];
}
}
return closestColor;
}
// Use a Set outside to track loaded fonts across multiple calls if this function were part of a larger app.
// For a single function call, this Set helps if ensureFontLoaded is called multiple times within one processImage call.
// For the given constraints "Output only the JavaScript function", it's better to have it self-contained.
const _loadedFonts = new Set();
async function ensureFontLoaded(fontFamily, fontFileUrl, weight = 'normal', style = 'normal', display = 'swap') {
const fontIdentifier = `${fontFamily}-${weight}-${style}`;
if (_loadedFonts.has(fontIdentifier) || document.fonts.check(`${style} ${weight} 12px "${fontFamily}"`)) {
return true;
}
const fontFace = new FontFace(fontFamily, `url(${fontFileUrl})`, { weight, style, display });
try {
await fontFace.load();
document.fonts.add(fontFace);
_loadedFonts.add(fontIdentifier);
// console.log(`Font "${fontFamily}" (${weight}, ${style}) loaded from ${fontFileUrl}`);
return true;
} catch (e) {
console.error(`Failed to load font "${fontFamily}" (${weight}, ${style}):`, e);
return false;
}
}
// --- Main Logic ---
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Ensure the image is loaded before trying to get its dimensions
if (!originalImg.complete || originalImg.naturalWidth === 0) {
// Fallback or error for unloaded image. For now, let's assume it's loaded.
// If it's critical, one might await originalImg.onload here, but it's passed as loaded.
console.error("Image not loaded or has zero dimensions.");
// Create a small placeholder canvas
canvas.width = 100;
canvas.height = 100;
ctx.fillStyle = 'red';
ctx.fillRect(0,0,100,100);
ctx.fillStyle = 'white';
ctx.fillText("Error: Image not loaded", 10, 50);
return canvas;
}
canvas.width = originalImg.naturalWidth || originalImg.width;
canvas.height = originalImg.naturalHeight || originalImg.height;
// Draw the original image
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
// Parse poster colors from string to RGB objects
const paletteRgbs = posterColorsStr.split(',').map(hex => hexToRgb(hex.trim()));
if (paletteRgbs.length === 0) { // Fallback if string is bad
paletteRgbs.push({r:0,g:0,b:0}, {r:255,g:255,b:255});
}
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;
const posterStep = (posterizeLevels > 1) ? 255 / (posterizeLevels - 1) : 0;
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i + 1];
let b = data[i + 2];
// 1. Contrast
if (contrastFactor !== 1.0) {
r = contrastFactor * (r - 128) + 128;
g = contrastFactor * (g - 128) + 128;
b = contrastFactor * (b - 128) + 128;
}
// 2. Posterization (Quantization)
if (posterizeLevels > 1 && posterStep > 0) {
r = Math.round(r / posterStep) * posterStep;
g = Math.round(g / posterStep) * posterStep;
b = Math.round(b / posterStep) * posterStep;
}
// Clamp values after contrast and posterization
r = Math.max(0, Math.min(255, r));
g = Math.max(0, Math.min(255, g));
b = Math.max(0, Math.min(255, b));
// 3. Map to limited palette
const closest = findClosestColor({ r, g, b }, paletteRgbs);
r = closest.r;
g = closest.g;
b = closest.b;
// 4. Noise (optional)
if (noiseAmount > 0) {
const noise = (Math.random() - 0.5) * 2 * 255 * noiseAmount;
r = Math.max(0, Math.min(255, r + noise));
g = Math.max(0, Math.min(255, g + noise));
b = Math.max(0, Math.min(255, b + noise));
}
data[i] = r;
data[i + 1] = g;
data[i + 2] = b;
// Alpha (data[i+3]) remains unchanged
}
ctx.putImageData(imageData, 0, 0);
// 5. Add Text Overlay
if (addText && addText.trim() !== "") {
// Attempt to load Oswald font if specified and potentially not available
if (textFont.toLowerCase().includes('oswald')) {
// URL for Oswald Bold (700 weight)
const oswaldBoldUrl = 'https://fonts.gstatic.com/s/oswald/v49/TK3_WkUHHAIjg75cFRf3bXL8LICs1_FvsUJiZTaR.woff2';
// Assume 'bold' or weight 700 is desired for Oswald in this style
await ensureFontLoaded('Oswald', oswaldBoldUrl, '700', 'normal');
}
ctx.font = textFont;
ctx.fillStyle = textColor;
ctx.textAlign = 'center';
// Adjust baseline based on Y position to make it more intuitive
if (textPositionYPercent < 0.2) { // Text near top
ctx.textBaseline = 'top';
} else if (textPositionYPercent > 0.8) { // Text near bottom
ctx.textBaseline = 'bottom';
} else { // Text in middle
ctx.textBaseline = 'middle';
}
const x = canvas.width * textPositionXPercent;
const y = canvas.height * textPositionYPercent;
if (textStrokeWidth > 0 && textStrokeColor) {
ctx.strokeStyle = textStrokeColor;
ctx.lineWidth = textStrokeWidth;
ctx.strokeText(addText, x, y);
}
ctx.fillText(addText, x, y);
}
return canvas;
}
Apply Changes