You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, posterizeLevels = 4, borderFactor = 1.0, paletteString = "#000000,#1E2D2F,#C09F58,#EAE0C8") {
// Helper function to parse color strings (hex, rgb, color names) into an {r, g, b, a} object.
function parseColor(colorStr) {
if (typeof colorStr !== 'string' || colorStr.trim() === "") {
// console.warn(`Invalid color string input: "${colorStr}". Defaulting to black.`);
return { r: 0, g: 0, b: 0, a: 255 };
}
const tempCanvas = document.createElement('canvas');
tempCanvas.width = 1;
tempCanvas.height = 1;
const tempCtx = tempCanvas.getContext('2d');
if (!tempCtx) { // Fallback if context cannot be created for some rare reason
// console.warn("Temporary canvas context for color parsing failed. Defaulting color to black.");
return { r: 0, g: 0, b: 0, a: 255 };
}
tempCtx.fillStyle = '#000'; // Initialize with a known opaque color
try {
tempCtx.fillStyle = colorStr.trim();
} catch (e) {
// console.warn(`Error setting fillStyle for color string: "${colorStr}". Defaulting to black.`, e);
return { r: 0, g: 0, b: 0, a: 255 };
}
const computedColor = tempCtx.fillStyle; // Will be #rrggbb or rgba(r,g,b,a)
if (computedColor.startsWith('#')) {
let hex = computedColor;
let r = 0, g = 0, b = 0;
if (hex.length === 4) { // #RGB
r = parseInt(hex[1] + hex[1], 16);
g = parseInt(hex[2] + hex[2], 16);
b = parseInt(hex[3] + hex[3], 16);
} else if (hex.length === 7) { // #RRGGBB
r = parseInt(hex.substring(1, 3), 16);
g = parseInt(hex.substring(3, 5), 16);
b = parseInt(hex.substring(5, 7), 16);
} else {
// console.warn(`Could not parse hex color: ${colorStr} (computed: ${computedColor}). Defaulting to black.`);
return { r: 0, g: 0, b: 0, a: 255 };
}
return { r, g, b, a: 255 };
} else if (computedColor.startsWith('rgb')) { // rgb(r, g, b) or rgba(r, g, b, a)
const parts = computedColor.substring(computedColor.indexOf('(') + 1, computedColor.lastIndexOf(')')).split(/,\s*/);
const rVal = parseInt(parts[0], 10);
const gVal = parseInt(parts[1], 10);
const bVal = parseInt(parts[2], 10);
const aVal = parts.length > 3 ? Math.round(parseFloat(parts[3]) * 255) : 255;
if (isNaN(rVal) || isNaN(gVal) || isNaN(bVal) || isNaN(aVal)) {
// console.warn(`Could not parse rgb/rgba color: ${colorStr} (computed: ${computedColor}). Defaulting to black.`);
return { r: 0, g: 0, b: 0, a: 255 };
}
return { r: rVal, g: gVal, b: bVal, a: aVal };
}
// console.warn(`Could not parse color: ${colorStr} (computed: ${computedColor}). Defaulting to black.`);
return { r: 0, g: 0, b: 0, a: 255 };
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) {
// console.error("Failed to get 2D context for processing image.");
// Return a very small canvas or handle error appropriately
const errorCanvas = document.createElement('canvas');
errorCanvas.width = 1; errorCanvas.height = 1;
return errorCanvas;
}
canvas.width = originalImg.naturalWidth || originalImg.width;
canvas.height = originalImg.naturalHeight || originalImg.height;
if (canvas.width === 0 || canvas.height === 0) {
return canvas; // Return empty canvas if image has no dimensions
}
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
let imageData;
try {
imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
} catch (e) {
// console.error("Could not get ImageData (e.g., CORS issue if image from another domain without CORS headers).", e);
// Draw a placeholder or return the original canvas
ctx.clearRect(0,0,canvas.width, canvas.height);
ctx.fillStyle = "grey";
ctx.fillRect(0,0,canvas.width, canvas.height);
ctx.fillStyle = "black";
ctx.textAlign = "center";
ctx.fillText("Error processing image", canvas.width/2, canvas.height/2);
return canvas;
}
const data = imageData.data;
const rawPalette = paletteString.split(',');
let artDecoPaletteRgb = rawPalette.map(colorStr => parseColor(colorStr.trim()));
// Filter out any malformed color objects (though parseColor defaults to black)
artDecoPaletteRgb = artDecoPaletteRgb.filter(c =>
c && typeof c.r === 'number' && typeof c.g === 'number' && typeof c.b === 'number' && typeof c.a === 'number'
);
if (artDecoPaletteRgb.length === 0) {
artDecoPaletteRgb.push(parseColor("#000000")); // Default to black
artDecoPaletteRgb.push(parseColor("#FFFFFF")); // and white, if palette is empty or fully unparseable
}
const numQuantLevels = Math.max(2, Math.floor(posterizeLevels)); // At least 2 levels (e.g., binary)
const R_LUMINANCE = 0.299;
const G_LUMINANCE = 0.587;
const B_LUMINANCE = 0.114;
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
// 1. Convert to grayscale
const gray = R_LUMINANCE * r + G_LUMINANCE * g + B_LUMINANCE * b;
// 2. Posterize grayscale value
const grayStep = 255 / (numQuantLevels - 1);
const posterizedGray = Math.round(Math.round(gray / grayStep) * grayStep);
// 3. Map posterized gray to a palette color
// Normalized gray (0 to 1) determines which color from the palette to pick.
// Palette is assumed to be ordered (e.g., dark to light by user).
const normalizedGray = Math.min(1, Math.max(0, posterizedGray / 255));
let paletteIndex;
if (artDecoPaletteRgb.length === 1) {
paletteIndex = 0;
} else {
// Map normalizedGray to an index in the palette array
// The (artDecoPaletteRgb.length - 1e-9) ensures that when normalizedGray is 1,
// it correctly maps to the last index after Math.floor.
paletteIndex = Math.floor(normalizedGray * (artDecoPaletteRgb.length - 1e-9));
paletteIndex = Math.min(paletteIndex, artDecoPaletteRgb.length - 1); // Clamp index
}
const chosenColor = artDecoPaletteRgb[paletteIndex];
data[i] = chosenColor.r;
data[i + 1] = chosenColor.g;
data[i + 2] = chosenColor.b;
// Alpha (data[i+3]) is preserved from original image
}
ctx.putImageData(imageData, 0, 0);
// 4. Add optional Art Deco style border
if (borderFactor > 0 && canvas.width > 0 && canvas.height > 0) {
let borderColorRgb = { r: 0, g: 0, b: 0 }; // Default border to black
if (artDecoPaletteRgb.length > 0) {
// Prefer a very dark color from the palette if available
let foundDark = artDecoPaletteRgb.find(c => (R_LUMINANCE * c.r + G_LUMINANCE * c.g + B_LUMINANCE * c.b) < 64); // Arbitrary "dark" threshold
if (foundDark) {
borderColorRgb = foundDark;
} else { // Otherwise, use the color with the overall minimum luminance from the palette
borderColorRgb = artDecoPaletteRgb.reduce((darkest, current) => {
const lumDarkest = R_LUMINANCE * darkest.r + G_LUMINANCE * darkest.g + B_LUMINANCE * darkest.b;
const lumCurrent = R_LUMINANCE * current.r + G_LUMINANCE * current.g + B_LUMINANCE * current.b;
return lumCurrent < lumDarkest ? current : darkest;
}, artDecoPaletteRgb[0]);
}
}
ctx.strokeStyle = `rgb(${borderColorRgb.r},${borderColorRgb.g},${borderColorRgb.b})`;
const minDimension = Math.min(canvas.width, canvas.height);
let baseLineWidth = Math.max(1, minDimension * 0.01); // Base thickness: 1% of min dimension
let lineWidth = Math.max(1, baseLineWidth * borderFactor);
lineWidth = Math.min(lineWidth, Math.max(1, minDimension * 0.1)); // Cap max border width (e.g., 10% of min dimension)
ctx.lineWidth = lineWidth;
// Draw outer border slightly inset so it's fully visible
ctx.strokeRect(lineWidth / 2, lineWidth / 2, canvas.width - lineWidth, canvas.height - lineWidth);
// Add an inner border line if borderFactor is high enough and space allows
const innerBorderFactorThreshold = 1.5;
if (borderFactor >= innerBorderFactorThreshold) {
const inset = lineWidth * 2.0; // Inset for the second border line
if (canvas.width > inset * 2 + lineWidth && canvas.height > inset * 2 + lineWidth) { // Ensure space for inner border
ctx.lineWidth = Math.max(1, lineWidth * 0.5); // Inner border can be thinner
ctx.strokeRect(inset, inset, canvas.width - 2 * inset, canvas.height - 2 * inset);
}
}
}
return canvas;
}
Apply Changes