You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, posterizationLevels = 6, outlineStrength = 1.2, outlineColor = '#302010', saturation = 0.8, sepia = 0.2, grainAmount = 0.1) {
// Clamp parameters to reasonable ranges for stability and quality
const pLevels = Math.max(2, Math.min(255, Math.floor(posterizationLevels)));
const sStrength = Math.max(0, outlineStrength);
const sat = Math.max(0, saturation);
const sep = Math.max(0, Math.min(1, sepia));
const grain = Math.max(0, Math.min(1, grainAmount));
const edgeThreshold = 50;
// --- 1. Canvas Setup ---
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const width = originalImg.naturalWidth;
const height = originalImg.naturalHeight;
canvas.width = width;
canvas.height = height;
// Draw image to get pixel data
ctx.drawImage(originalImg, 0, 0, width, height);
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
const originalData = Uint8ClampedArray.from(data); // Create an unmodified copy for edge detection
// --- 2. Color Processing (Posterize, Sepia, Saturate) ---
// This loop applies the core color palette reduction and vintage tinting
const step = 255 / (pLevels - 1);
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i + 1];
let b = data[i + 2];
// Posterize: Reduce the number of colors for a cartoon look
r = Math.round(r / step) * step;
g = Math.round(g / step) * step;
b = Math.round(b / step) * step;
// Sepia: Add a warm, aged tint
if (sep > 0) {
const sr = r * (1 - 0.607 * sep) + g * (0.769 * sep) + b * (0.189 * sep);
const sg = r * (0.349 * sep) + g * (1 - 0.314 * sep) + b * (0.168 * sep);
const sb = r * (0.272 * sep) + g * (0.534 * sep) + b * (1 - 0.869 * sep);
r = sr; g = sg; b = sb;
}
// Saturation: Mute the colors for a classic feel
if (sat !== 1) {
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
r = gray * (1 - sat) + r * sat;
g = gray * (1 - sat) + g * sat;
b = gray * (1 - sat) + b * sat;
}
data[i] = r;
data[i + 1] = g;
data[i + 2] = b;
}
ctx.putImageData(imageData, 0, 0);
// --- 3. Edge Detection (Sobel) for Outlines ---
// Creates the hand-drawn outline effect typical of cel animation
if (sStrength > 0) {
const outlineCanvas = document.createElement('canvas');
outlineCanvas.width = width;
outlineCanvas.height = height;
const outlineCtx = outlineCanvas.getContext('2d');
const rC = parseInt(outlineColor.slice(1, 3), 16);
const gC = parseInt(outlineColor.slice(3, 5), 16);
const bC = parseInt(outlineColor.slice(5, 7), 16);
const getIntensity = (d, x, y, w) => {
const i = (y * w + x) * 4;
// Luminosity method for grayscale conversion
return 0.299 * d[i] + 0.587 * d[i + 1] + 0.114 * d[i + 2];
};
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 < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
let pixelX = 0;
let pixelY = 0;
for (let j = -1; j <= 1; j++) {
for (let i = -1; i <= 1; i++) {
const intensity = getIntensity(originalData, x + i, y + j, width);
pixelX += intensity * sobelX[j + 1][i + 1];
pixelY += intensity * sobelY[j + 1][i + 1];
}
}
const magnitude = Math.sqrt(pixelX * pixelX + pixelY * pixelY);
if (magnitude > edgeThreshold) {
const opacity = Math.min(1, magnitude / 150);
outlineCtx.fillStyle = `rgba(${rC}, ${gC}, ${bC}, ${opacity})`;
// Add random wobble to simulate hand-drawn imperfection
const wobbleX = (Math.random() - 0.5) * sStrength;
const wobbleY = (Math.random() - 0.5) * sStrength;
// Draw a small circle for each edge pixel to build up the line
outlineCtx.beginPath();
outlineCtx.arc(x + wobbleX, y + wobbleY, sStrength / 2, 0, Math.PI * 2);
outlineCtx.fill();
}
}
}
ctx.drawImage(outlineCanvas, 0, 0);
}
// --- 4. Grain / Texture Overlay ---
// Adds a subtle paper or film grain texture
if (grain > 0) {
const grainCanvas = document.createElement('canvas');
grainCanvas.width = width;
grainCanvas.height = height;
const grainCtx = grainCanvas.getContext('2d');
const grainImageData = grainCtx.createImageData(width, height);
const grainData = grainImageData.data;
for (let i = 0; i < grainData.length; i += 4) {
const val = Math.random() * 255;
grainData[i] = val;
grainData[i+1] = val;
grainData[i+2] = val;
grainData[i+3] = 255;
}
grainCtx.putImageData(grainImageData, 0, 0);
ctx.globalAlpha = grain;
ctx.globalCompositeOperation = 'soft-light';
ctx.drawImage(grainCanvas, 0, 0);
// Reset canvas context properties
ctx.globalAlpha = 1.0;
ctx.globalCompositeOperation = 'source-over';
}
// --- 5. Return Final Canvas ---
return canvas;
}
Apply Changes