You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, colorMode = "ukiyo", customPaletteCSV = "", posterizationLevels = 5, edgeThreshold = 70, outlineColor = "#000000") {
// Helper function: Convert hex to RGB
// Includes basic validation and default for invalid hex.
function hexToRgb(hex) {
if (!hex || typeof hex !== 'string' || !hex.startsWith('#') || (hex.length !== 4 && hex.length !== 7)) {
// console.warn(`Invalid hex: ${hex}, defaulting to black.`);
return { r: 0, g: 0, b: 0 }; // Default to black for invalid hex
}
let hexVal = hex.slice(1);
if (hexVal.length === 3) {
hexVal = hexVal.split('').map(char => char + char).join('');
}
let bigint = parseInt(hexVal, 16);
let r = (bigint >> 16) & 255;
let g = (bigint >> 8) & 255;
let b = bigint & 255;
if (isNaN(r) || isNaN(g) || isNaN(b)) {
// console.warn(`NaN parsing hex: ${hex}, defaulting to black.`);
return { r: 0, g: 0, b: 0 };
}
return { r, g, b };
}
// Helper function: Calculate color distance (squared Euclidean distance for efficiency)
function colorDistanceSquared(rgb1, rgb2) {
let dr = rgb1.r - rgb2.r;
let dg = rgb1.g - rgb2.g;
let db = rgb1.b - rgb2.b;
return dr * dr + dg * dg + db * db;
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
canvas.width = originalImg.naturalWidth || originalImg.width;
canvas.height = originalImg.naturalHeight || originalImg.height;
if (canvas.width === 0 || canvas.height === 0) {
console.error("Image has zero dimensions. Cannot process.");
// Optionally, return a small, identifiable canvas or throw error
const errCanvas = document.createElement('canvas');
errCanvas.width = 100; errCanvas.height = 30;
const errCtx = errCanvas.getContext('2d');
errCtx.fillStyle = 'red'; errCtx.fillRect(0,0,100,30);
errCtx.fillStyle = 'white'; errCtx.fillText("Error: Zero dim", 5, 20);
return errCanvas;
}
// Draw original image to a temporary canvas to get its imageData
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 originalData = originalImageData.data;
const processedColorImageData = tempCtx.createImageData(canvas.width, canvas.height);
const processedColorData = processedColorImageData.data;
// 1. Color Processing
let activePalette = [];
let usePosterization = false;
let localColorMode = colorMode;
if (localColorMode === "custom" && customPaletteCSV && customPaletteCSV.trim() !== "") {
try {
activePalette = customPaletteCSV.split(',')
.map(hex => hex.trim())
.filter(hex => hex.length > 0)
.map(hex => hexToRgb(hex));
if (activePalette.some(c => typeof c.r === 'undefined' || isNaN(c.r))) {
console.warn("Custom palette contains invalid colors. Falling back to 'ukiyo' mode.");
activePalette = [];
localColorMode = "ukiyo";
} else if (activePalette.length === 0 && customPaletteCSV.trim() !== "") {
console.warn("Custom palette string was provided but resulted in no valid colors. Falling back to 'ukiyo' mode.");
localColorMode = "ukiyo";
}
} catch (e) {
console.warn("Error parsing custom palette CSV. Falling back to 'ukiyo' mode.", e);
activePalette = [];
localColorMode = "ukiyo";
}
}
if (localColorMode === "ukiyo" && activePalette.length === 0) {
const defaultUkiyoPaletteSrc = [
"#2F2F2F", "#F5E9D3", "#E0B7A0", "#B07050",
"#4D6A85", "#607850", "#C85050"
];
activePalette = defaultUkiyoPaletteSrc.map(hex => hexToRgb(hex.trim()));
}
if (activePalette.length === 0) {
usePosterization = true;
}
for (let i = 0; i < originalData.length; i += 4) {
let r = originalData[i];
let g = originalData[i + 1];
let b = originalData[i + 2];
const a = originalData[i + 3];
if (!usePosterization) { // Palette mapping
let minDistance = Infinity;
let closestColor = activePalette[0];
for (const palColor of activePalette) {
const distance = colorDistanceSquared({ r, g, b }, palColor);
if (distance < minDistance) {
minDistance = distance;
closestColor = palColor;
}
}
processedColorData[i] = closestColor.r;
processedColorData[i + 1] = closestColor.g;
processedColorData[i + 2] = closestColor.b;
} else { // Posterization
const levels = Math.max(2, posterizationLevels);
const step = 255 / (levels - 1);
processedColorData[i] = Math.max(0, Math.min(255,Math.round(Math.round(r / step) * step)));
processedColorData[i + 1] = Math.max(0, Math.min(255,Math.round(Math.round(g / step) * step)));
processedColorData[i + 2] = Math.max(0, Math.min(255,Math.round(Math.round(b / step) * step)));
}
processedColorData[i + 3] = a;
}
ctx.putImageData(processedColorImageData, 0, 0);
// 2. Edge Detection (Sobel on grayscale version of original image)
const grayscaleData = new Uint8ClampedArray(canvas.width * canvas.height);
for (let i = 0, j = 0; i < originalData.length; i += 4, j++) {
const r_orig = originalData[i];
const g_orig = originalData[i + 1];
const b_orig = originalData[i + 2];
grayscaleData[j] = Math.round(0.299 * r_orig + 0.587 * g_orig + 0.114 * b_orig);
}
const edgeMaskImageData = tempCtx.createImageData(canvas.width, canvas.height);
const edgeMaskData = edgeMaskImageData.data;
const { r: outlineR, g: outlineG, b: outlineB } = hexToRgb(outlineColor);
const Gx = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]];
const Gy = [[-1, -2, -1], [0, 0, 0], [1, 2, 1]];
for (let y = 1; y < canvas.height - 1; y++) {
for (let x = 1; x < canvas.width - 1; x++) {
let sumX = 0;
let sumY = 0;
for (let ky = -1; ky <= 1; ky++) {
for (let kx = -1; kx <= 1; kx++) {
const grayVal = grayscaleData[(y + ky) * canvas.width + (x + kx)];
sumX += grayVal * Gx[ky + 1][kx + 1];
sumY += grayVal * Gy[ky + 1][kx + 1];
}
}
const magnitude = Math.sqrt(sumX * sumX + sumY * sumY);
const destIdx = (y * canvas.width + x) * 4;
if (magnitude > edgeThreshold) {
edgeMaskData[destIdx] = outlineR;
edgeMaskData[destIdx + 1] = outlineG;
edgeMaskData[destIdx + 2] = outlineB;
edgeMaskData[destIdx + 3] = 255;
} else {
edgeMaskData[destIdx] = 0;
edgeMaskData[destIdx + 1] = 0;
edgeMaskData[destIdx + 2] = 0;
edgeMaskData[destIdx + 3] = 0;
}
}
}
const edgeCanvas = document.createElement('canvas');
edgeCanvas.width = canvas.width;
edgeCanvas.height = canvas.height;
const edgeCtx = edgeCanvas.getContext('2d');
edgeCtx.putImageData(edgeMaskImageData, 0, 0);
ctx.drawImage(edgeCanvas, 0, 0);
return canvas;
}
Apply Changes