You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, dotSize = 8, outlineThreshold = 100, paletteColorsStr = "255,0,0;255,255,0;0,0,255;0,0,0;255,255,255;253,224,200", skinDotColorStr = "255,0,0", primaryDotColorStr = "0,0,0") {
const w = originalImg.width;
const h = originalImg.height;
// 1. Initialize canvases and contexts
const outputCanvas = document.createElement('canvas');
outputCanvas.width = w;
outputCanvas.height = h;
const outputCtx = outputCanvas.getContext('2d');
const tempCanvas = document.createElement('canvas');
tempCanvas.width = w;
tempCanvas.height = h;
const tempCtx = tempCanvas.getContext('2d');
// Helper to parse color strings (e.g., "255,0,0") to {r,g,b}
function parseColor(str) {
const parts = str.split(',').map(Number);
if (parts.length === 3 && parts.every(num => !isNaN(num) && num >= 0 && num <= 255)) {
return { r: parts[0], g: parts[1], b: parts[2] };
}
// Fallback to black if parsing fails
return { r: 0, g: 0, b: 0 };
}
// Helper to format {r,g,b} to "rgb(r,g,b)" string
function toRGBString(colorObj) {
return `rgb(${colorObj.r},${colorObj.g},${colorObj.b})`;
}
// Predefined named colors for special logic, using default values
const DEFAULT_COLORS_NAMED_MAP = {
"255,0,0": "red",
"255,255,0": "yellow",
"0,0,255": "blue",
"0,0,0": "black",
"255,255,255": "white",
"253,224,200": "skin" // Default skin tone
};
// Parse input palette string
const palette = paletteColorsStr.split(';').map(s => {
const p = parseColor(s);
const sNormalized = `${p.r},${p.g},${p.b}`; // Normalize for map lookup
const name = DEFAULT_COLORS_NAMED_MAP[sNormalized] || "custom";
return { ...p, name: name };
});
if (palette.length === 0) { // Ensure palette is not empty
palette.push({ r:0, g:0, b:0, name: "black"}); // Default to black if palette is empty
}
const skinColorEntry = palette.find(c => c.name === "skin");
const whiteColorEntry = palette.find(c => c.name === "white");
const blackColorEntry = palette.find(c => c.name === "black");
const skinDotRGB = parseColor(skinDotColorStr);
const primaryDotRGB = parseColor(primaryDotColorStr);
// Helper function: Euclidean distance for colors
function colorDistance(c1, c2) {
return Math.sqrt(Math.pow(c1.r - c2.r, 2) + Math.pow(c1.g - c2.g, 2) + Math.pow(c1.b - c2.b, 2));
}
// Helper function: Find closest color in palette
function getClosestPaletteColor(r, g, b) {
let closestColor = palette[0];
let minDistance = Infinity;
for (const pColor of palette) {
const dist = colorDistance({r,g,b}, pColor);
if (dist < minDistance) {
minDistance = dist;
closestColor = pColor;
}
}
return closestColor;
}
// 2. Posterization and Grayscale original for intensity
tempCtx.drawImage(originalImg, 0, 0, w, h);
const originalImgData = tempCtx.getImageData(0, 0, w, h);
const posterizedImgData = tempCtx.createImageData(w, h);
const originalLuminanceMap = new Uint8ClampedArray(w * h);
for (let i = 0; i < originalImgData.data.length; i += 4) {
const r = originalImgData.data[i];
const g = originalImgData.data[i+1];
const b = originalImgData.data[i+2];
originalLuminanceMap[i / 4] = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
const closest = getClosestPaletteColor(r, g, b);
posterizedImgData.data[i] = closest.r;
posterizedImgData.data[i+1] = closest.g;
posterizedImgData.data[i+2] = closest.b;
posterizedImgData.data[i+3] = 255;
}
// 3. Halftone Dots
for (let y = 0; y < h; y += dotSize) {
for (let x = 0; x < w; x += dotSize) {
const sampleX = Math.floor(Math.min(x + dotSize / 2, w - 1));
const sampleY = Math.floor(Math.min(y + dotSize / 2, h - 1));
const pIndex = (sampleY * w + sampleX) * 4;
const cellPosterizedColor_r = posterizedImgData.data[pIndex];
const cellPosterizedColor_g = posterizedImgData.data[pIndex+1];
const cellPosterizedColor_b = posterizedImgData.data[pIndex+2];
const currentPosterizedPaletteEntry = palette.find(p => p.r === cellPosterizedColor_r && p.g === cellPosterizedColor_g && p.b === cellPosterizedColor_b)
|| { name: "custom", r: cellPosterizedColor_r, g: cellPosterizedColor_g, b: cellPosterizedColor_b };
const originalCellLuminance = originalLuminanceMap[sampleY * w + sampleX];
let cellBgFill = toRGBString(currentPosterizedPaletteEntry);
let dotFill = toRGBString(primaryDotRGB);
let drawThisDot = true;
if (whiteColorEntry && currentPosterizedPaletteEntry.name === "white") {
cellBgFill = toRGBString(whiteColorEntry);
drawThisDot = false;
} else if (blackColorEntry && currentPosterizedPaletteEntry.name === "black") {
cellBgFill = toRGBString(blackColorEntry);
drawThisDot = false;
} else if (skinColorEntry && currentPosterizedPaletteEntry.name === "skin") {
cellBgFill = whiteColorEntry ? toRGBString(whiteColorEntry) : 'rgb(255,255,255)';
dotFill = toRGBString(skinDotRGB);
}
// Default: primary colors use posterized color as bg, primaryDotRGB for dots.
outputCtx.fillStyle = cellBgFill;
outputCtx.fillRect(x, y, dotSize, dotSize);
if (drawThisDot) {
const intensity = originalCellLuminance / 255.0;
let radius = (dotSize / 2.1) * (1.0 - intensity);
radius = Math.max(0, radius);
if (radius > dotSize * 0.05) {
outputCtx.fillStyle = dotFill;
outputCtx.beginPath();
outputCtx.arc(x + dotSize / 2, y + dotSize / 2, radius, 0, 2 * Math.PI);
outputCtx.fill();
}
}
}
}
// 4. Outlines (Sobel filter)
const outlineCanvas = document.createElement('canvas');
outlineCanvas.width = w;
outlineCanvas.height = h;
const outlineCtx = outlineCanvas.getContext('2d');
tempCtx.drawImage(originalImg, 0, 0, w, h);
const sourceForSobelGray = tempCtx.getImageData(0, 0, w, h);
const grayscaleImgDataForSobel = tempCtx.createImageData(w, h);
for (let i = 0; i < sourceForSobelGray.data.length; i += 4) {
const r = sourceForSobelGray.data[i];
const g = sourceForSobelGray.data[i+1];
const b = sourceForSobelGray.data[i+2];
const avg = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
grayscaleImgDataForSobel.data[i] = avg;
grayscaleImgDataForSobel.data[i+1] = avg;
grayscaleImgDataForSobel.data[i+2] = avg;
grayscaleImgDataForSobel.data[i+3] = 255;
}
const sobelFinalData = outlineCtx.createImageData(w, h);
const GxMatrix = [[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]];
const GyMatrix = [[-1, -2, -1], [0, 0, 0], [1, 2, 1]];
function getPixelGrayscale(imgData, x_coord, y_coord) {
if (x_coord < 0 || x_coord >= imgData.width || y_coord < 0 || y_coord >= imgData.height) return 0;
return imgData.data[(y_coord * imgData.width + x_coord) * 4];
}
for (let y_idx = 0; y_idx < h; y_idx++) {
for (let x_idx = 0; x_idx < w; x_idx++) {
let Gx = 0;
let Gy = 0;
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
const pixelVal = getPixelGrayscale(grayscaleImgDataForSobel, x_idx + j - 1, y_idx + i - 1);
Gx += pixelVal * GxMatrix[i][j];
Gy += pixelVal * GyMatrix[i][j];
}
}
const magnitude = Math.sqrt(Gx * Gx + Gy * Gy);
const K_idx = (y_idx * w + x_idx) * 4;
if (magnitude > outlineThreshold) {
sobelFinalData.data[K_idx] = 0;
sobelFinalData.data[K_idx + 1] = 0;
sobelFinalData.data[K_idx + 2] = 0;
sobelFinalData.data[K_idx + 3] = 255;
} else {
sobelFinalData.data[K_idx + 3] = 0;
}
}
}
outlineCtx.putImageData(sobelFinalData, 0, 0);
outputCtx.drawImage(outlineCanvas, 0, 0);
return outputCanvas;
}
Apply Changes