You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, simplification = 5, saturation = 1.4, outlineThreshold = 40) {
const width = originalImg.width;
const height = originalImg.height;
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
ctx.drawImage(originalImg, 0, 0, width, height);
const originalImageData = ctx.getImageData(0, 0, width, height);
const data = originalImageData.data;
// Helper function to convert RGB to HSL
const 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;
} 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 function to convert HSL to RGB
const hslToRgb = (h, s, l) => {
let r, g, b;
if (s === 0) {
r = g = b = l;
} else {
const 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 [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
};
// --- 1. Edge-preserving smoothing (Kuwahara filter) for the marker fill effect ---
// This is a complex filter that smooths while preserving edges, creating a painterly look.
const radius = Math.max(1, Math.min(Math.floor(simplification), 15));
const smoothedData = new Uint8ClampedArray(data.length);
const get_pixel = (x, y) => (y * width + x) * 4;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const sums = [[0, 0, 0],[0, 0, 0],[0, 0, 0],[0, 0, 0]];
const sqSums = [[0, 0, 0],[0, 0, 0],[0, 0, 0],[0, 0, 0]];
const counts = [0, 0, 0, 0];
for (let j = -radius; j <= radius; j++) {
for (let i = -radius; i <= radius; i++) {
const sampleX = Math.max(0, Math.min(width - 1, x + i));
const sampleY = Math.max(0, Math.min(height - 1, y + j));
const idx = get_pixel(sampleX, sampleY);
const r = data[idx], g = data[idx + 1], b = data[idx + 2];
let quadrant = -1;
if (i <= 0 && j <= 0) quadrant = 0; // Top-left
else if (i > 0 && j <= 0) quadrant = 1; // Top-right
else if (i <= 0 && j > 0) quadrant = 2; // Bottom-left
else if (i > 0 && j > 0) quadrant = 3; // Bottom-right
sums[quadrant][0] += r;
sums[quadrant][1] += g;
sums[quadrant][2] += b;
sqSums[quadrant][0] += r * r;
sqSums[quadrant][1] += g * g;
sqSums[quadrant][2] += b * b;
counts[quadrant]++;
}
}
let minVariance = -1;
let bestQuadrant = -1;
for (let q = 0; q < 4; q++) {
if(counts[q] === 0) continue;
const meanR = sums[q][0] / counts[q];
const meanG = sums[q][1] / counts[q];
const meanB = sums[q][2] / counts[q];
const varianceR = sqSums[q][0] / counts[q] - meanR * meanR;
const varianceG = sqSums[q][1] / counts[q] - meanG * meanG;
const varianceB = sqSums[q][2] / counts[q] - meanB * meanB;
const variance = varianceR + varianceG + varianceB;
if (minVariance === -1 || variance < minVariance) {
minVariance = variance;
bestQuadrant = q;
}
}
const outIdx = get_pixel(x, y);
smoothedData[outIdx] = sums[bestQuadrant][0] / counts[bestQuadrant];
smoothedData[outIdx + 1] = sums[bestQuadrant][1] / counts[bestQuadrant];
smoothedData[outIdx + 2] = sums[bestQuadrant][2] / counts[bestQuadrant];
smoothedData[outIdx + 3] = 255;
}
}
// --- 2. Increase saturation for vibrant marker colors ---
for (let i = 0; i < smoothedData.length; i += 4) {
let [h, s, l] = rgbToHsl(smoothedData[i], smoothedData[i+1], smoothedData[i+2]);
s = Math.min(1, s * saturation);
const [r, g, b] = hslToRgb(h, s, l);
smoothedData[i] = r;
smoothedData[i + 1] = g;
smoothedData[i + 2] = b;
}
// --- 3. Edge detection (Sobel) for marker outlines ---
const grayscaleData = new Uint8ClampedArray(width * height);
for (let i = 0, j = 0; i < data.length; i += 4, j++) {
grayscaleData[j] = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114;
}
const outlineData = new Uint8ClampedArray(data.length).fill(0);
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, pixelY = 0;
for (let j = -1; j <= 1; j++) {
for (let i = -1; i <= 1; i++) {
const grayValue = grayscaleData[(y + j) * width + (x + i)];
pixelX += grayValue * sobelX[j + 1][i + 1];
pixelY += grayValue * sobelY[j + 1][i + 1];
}
}
const magnitude = Math.sqrt(pixelX * pixelX + pixelY * pixelY);
if (magnitude > outlineThreshold) {
const idx = (y * width + x) * 4;
outlineData[idx] = 0; // R
outlineData[idx + 1] = 0; // G
outlineData[idx + 2] = 0; // B
outlineData[idx + 3] = 120 + Math.min(135, magnitude); // Alpha based on strength
}
}
}
// --- 4. Composite the layers ---
// Place the smoothed and saturated base image
ctx.putImageData(new ImageData(smoothedData, width, height), 0, 0);
// Create a temporary canvas for the outlines
const outlineCanvas = document.createElement('canvas');
outlineCanvas.width = width;
outlineCanvas.height = height;
const outlineCtx = outlineCanvas.getContext('2d');
outlineCtx.putImageData(new ImageData(outlineData, width, height), 0, 0);
// Draw the outlines on top of the base image
ctx.drawImage(outlineCanvas, 0, 0);
return canvas;
}
Apply Changes