You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, paletteColorsStr = "00529B,40E0D0,C8143C,008000,DAA520,F5DCC4,8B4513,FFFAE6", outlineColorHex = "361E14", outlineThreshold = 50, posterizeLevels = 5) {
// 1. Canvas Setup
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Use naturalWidth/Height for <img> elements, fallback to width/height for Image objects
canvas.width = originalImg.naturalWidth || originalImg.width;
canvas.height = originalImg.naturalHeight || originalImg.height;
if (canvas.width === 0 || canvas.height === 0) {
console.error("Image has zero width or height. Ensure the image is loaded before processing.");
// Return an empty (or minimally sized) canvas to avoid errors downstream
canvas.width = canvas.width || 1;
canvas.height = canvas.height || 1;
return canvas;
}
// Helper: Parse Hex to RGB
function hexToRgb(hex) {
hex = hex.replace(/^#/, '');
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
const bigint = parseInt(hex, 16);
if (isNaN(bigint)) {
console.warn(`Invalid hex color: "${hex}", defaulting to black.`);
return { r: 0, g: 0, b: 0 };
}
return { r: (bigint >> 16) & 255, g: (bigint >> 8) & 255, b: bigint & 255 };
}
// Helper: Color distance squared (avoids Math.sqrt for faster comparisons)
function colorDistanceSquared(c1, c2) {
return Math.pow(c1.r - c2.r, 2) + Math.pow(c1.g - c2.g, 2) + Math.pow(c1.b - c2.b, 2);
}
// Helper: Find closest color in palette
function findClosestPaletteColor(rgb, palette) {
if (!palette || palette.length === 0) {
return rgb; // Should not happen with default and fallback
}
let closestColor = palette[0];
let minDistance = colorDistanceSquared(rgb, closestColor);
for (let i = 1; i < palette.length; i++) {
const distance = colorDistanceSquared(rgb, palette[i]);
if (distance < minDistance) {
minDistance = distance;
closestColor = palette[i];
}
if (minDistance === 0) break; // Exact match
}
return closestColor;
}
// Parse parameters
let finalPalette = paletteColorsStr.split(',')
.map(hex => hex.trim())
.filter(hex => hex.length > 0)
.map(hex => hexToRgb(hex));
if (finalPalette.length === 0) {
console.warn("Provided palette string resulted in an empty palette. Using a default internal palette.");
// Fallback to simple black & white if parsing fails completely
finalPalette = [{ r: 0, g: 0, b: 0 }, { r: 255, g: 255, b: 255 }];
}
const outlineRgb = hexToRgb(outlineColorHex);
const pLevels = Math.max(2, Math.round(posterizeLevels)); // Ensure integer and at least 2 levels
// --- Step 1: Color Quantization/Posterization and Palette Mapping ---
const colorCanvas = document.createElement('canvas');
const colorCtx = colorCanvas.getContext('2d', { willReadFrequently: true });
colorCanvas.width = canvas.width;
colorCanvas.height = canvas.height;
colorCtx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
const imageData = colorCtx.getImageData(0, 0, colorCanvas.width, colorCanvas.height);
const data = imageData.data;
const posterizationFactor = (pLevels > 1) ? (255 / (pLevels - 1)) : 255; // Avoid division by zero if pLevels is 1 (though we enforced >=2)
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i+1];
let b = data[i+2];
// Apply posterization
if (pLevels > 1) {
r = Math.round(Math.round(r / posterizationFactor) * posterizationFactor);
g = Math.round(Math.round(g / posterizationFactor) * posterizationFactor);
b = Math.round(Math.round(b / posterizationFactor) * posterizationFactor);
}
const closest = findClosestPaletteColor({ r, g, b }, finalPalette);
data[i] = closest.r;
data[i + 1] = closest.g;
data[i + 2] = closest.b;
// Alpha (data[i+3]) remains unchanged
}
colorCtx.putImageData(imageData, 0, 0);
// --- Step 2: Edge Detection (Sobel on original grayscale) ---
// Temporary canvas for original image data for grayscale conversion
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
tempCanvas.width = canvas.width;
tempCanvas.height = canvas.height;
tempCtx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
const originalImageData = tempCtx.getImageData(0, 0, canvas.width, canvas.height);
const grayData = new Uint8ClampedArray(canvas.width * canvas.height); // Store grayscale values
for (let i = 0; i < originalImageData.data.length; i += 4) {
const r_orig = originalImageData.data[i];
const g_orig = originalImageData.data[i+1];
const b_orig = originalImageData.data[i+2];
grayData[i / 4] = Math.round(0.299 * r_orig + 0.587 * g_orig + 0.114 * b_orig); // Luminance
}
// Canvas for drawing edges
const edgeCanvas = document.createElement('canvas');
const edgeCtx = edgeCanvas.getContext('2d');
edgeCanvas.width = canvas.width;
edgeCanvas.height = canvas.height;
const edgePixelImageData = edgeCtx.createImageData(canvas.width, canvas.height);
const edgePixelData = edgePixelImageData.data;
const sobelX = [
[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]
];
const sobelY = [
[-1, -2, -1],
[0, 0, 0],
[1, 2, 1]
];
for (let y = 1; y < canvas.height - 1; y++) { // Skip 1-pixel border
for (let x = 1; x < canvas.width - 1; x++) { // Skip 1-pixel border
let gx = 0;
let gy = 0;
for (let ky = -1; ky <= 1; ky++) {
for (let kx = -1; kx <= 1; kx++) {
const pixelIndex = (y + ky) * canvas.width + (x + kx);
const pixelVal = grayData[pixelIndex];
gx += pixelVal * sobelX[ky + 1][kx + 1];
gy += pixelVal * sobelY[ky + 1][kx + 1];
}
}
const magnitude = Math.sqrt(gx * gx + gy * gy);
const idx = (y * canvas.width + x) * 4;
if (magnitude > outlineThreshold) {
edgePixelData[idx] = outlineRgb.r;
edgePixelData[idx + 1] = outlineRgb.g;
edgePixelData[idx + 2] = outlineRgb.b;
edgePixelData[idx + 3] = 255; // Opaque outline
} else {
edgePixelData[idx + 3] = 0; // Transparent if not an edge
}
}
}
edgeCtx.putImageData(edgePixelImageData, 0, 0);
// --- Step 3: Combine ---
ctx.drawImage(colorCanvas, 0, 0); // Draw color-mapped image
ctx.drawImage(edgeCanvas, 0, 0); // Draw outlines on top
return canvas;
}
Apply Changes