You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, posterizeLevels = 5, outlineColorStr = "0,0,0", warmth = 20, saturationBoost = 1.1) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
canvas.width = originalImg.width;
canvas.height = originalImg.height;
// Helper to clamp values
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
// Ensure posterizeLevels is at least 2
const actualPosterizeLevels = Math.max(2, Math.floor(posterizeLevels));
// Helper: Posterize a single color channel
function posterizeChannel(value, levels) {
if (levels <= 1) return value; // Should not happen due to Math.max(2,...)
const step = 255 / (levels - 1);
return Math.round(value / step) * step;
}
// Helper: RGB to HSL conversion
// r, g, b in [0, 255]
// Returns h, s, l in [0, 1]
function rgbToHsl(r, g, b) {
r /= 255; g /= 255; b /= 255;
const max = Math.max(r, g, b), min = Math.min(r, g, b);
let h, s, l = (max + min) / 2;
if (max === min) {
h = s = 0; // achromatic
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return { h, s, l };
}
// Helper: HSL to RGB conversion
// h, s, l in [0, 1]
// Returns r, g, b in [0, 255]
function hslToRgb(h, s, l) {
let r, g, b;
if (s === 0) {
r = g = b = l; // achromatic
} else {
function hue2rgb(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
}
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255)
};
}
// Parse outlineColorStr
const [oR, oG, oB] = outlineColorStr.split(',').map(s => parseInt(s.trim(), 10));
// Draw original image to canvas to get its pixel data
ctx.drawImage(originalImg, 0, 0);
const originalImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const originalData = originalImageData.data;
const outputImageData = ctx.createImageData(canvas.width, canvas.height);
const outputData = outputImageData.data;
// This will store only posterized values, without tint/saturation, for edge detection
const posterizedOnlyData = new Uint8ClampedArray(originalData.length);
const width = canvas.width;
const height = canvas.height;
// Step 1: Posterize, apply warmth, and saturation. Store results.
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];
// Posterize
const pr = posterizeChannel(r, actualPosterizeLevels);
const pg = posterizeChannel(g, actualPosterizeLevels);
const pb = posterizeChannel(b, actualPosterizeLevels);
posterizedOnlyData[i] = pr;
posterizedOnlyData[i+1] = pg;
posterizedOnlyData[i+2] = pb;
posterizedOnlyData[i+3] = a; // Preserve alpha
// Apply warmth to posterized colors
let warmedR = clamp(pr + warmth, 0, 255);
let warmedG = clamp(pg + warmth * 0.7, 0, 255); // Less warmth for green
let warmedB = clamp(pb - warmth * 0.3, 0, 255); // Cool down blue slightly
// Apply saturation boost
let hsl = rgbToHsl(warmedR, warmedG, warmedB);
hsl.s = clamp(hsl.s * saturationBoost, 0, 1);
let finalRgb = hslToRgb(hsl.h, hsl.s, hsl.l);
outputData[i] = finalRgb.r;
outputData[i+1] = finalRgb.g;
outputData[i+2] = finalRgb.b;
outputData[i+3] = a; // Preserve alpha
}
// Step 2: Detect edges from the posterizedOnlyData
const edgeMap = new Uint8Array(width * height); // 0 for no edge, 1 for edge
const edgeThreshold = 1; // Any difference in posterized color value indicates an edge
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const currentPixelIndex = (y * width + x) * 4;
const r1 = posterizedOnlyData[currentPixelIndex];
const g1 = posterizedOnlyData[currentPixelIndex + 1];
const b1 = posterizedOnlyData[currentPixelIndex + 2];
// Check right neighbor
if (x < width - 1) {
const rightPixelIndex = (y * width + (x + 1)) * 4;
const r2 = posterizedOnlyData[rightPixelIndex];
const g2 = posterizedOnlyData[rightPixelIndex + 1];
const b2 = posterizedOnlyData[rightPixelIndex + 2];
if (Math.abs(r1 - r2) > edgeThreshold || Math.abs(g1 - g2) > edgeThreshold || Math.abs(b1 - b2) > edgeThreshold) {
edgeMap[y * width + x] = 1;
}
}
// Check bottom neighbor
if (y < height - 1) {
const bottomPixelIndex = ((y + 1) * width + x) * 4;
const r2 = posterizedOnlyData[bottomPixelIndex];
const g2 = posterizedOnlyData[bottomPixelIndex + 1];
const b2 = posterizedOnlyData[bottomPixelIndex + 2];
if (Math.abs(r1 - r2) > edgeThreshold || Math.abs(g1 - g2) > edgeThreshold || Math.abs(b1 - b2) > edgeThreshold) {
edgeMap[y * width + x] = 1;
}
}
}
}
// Step 3: Draw the processed base image (posterized, warmed, saturated)
ctx.putImageData(outputImageData, 0, 0);
// Step 4: Draw outlines on top
ctx.fillStyle = `rgb(${oR},${oG},${oB})`;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
if (edgeMap[y * width + x] === 1) {
ctx.fillRect(x, y, 1, 1); // Draw a 1px outline
}
}
}
return canvas;
}
Apply Changes