You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, lineColor = "#3e3838", paperTexture = 1, highlightThreshold = 200, saturation = 0.8) {
const width = originalImg.width;
const height = originalImg.height;
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
// Parse line color
const hexToRgb = hex => {
let c;
if(/^#([A-Fa-f0-9]{3}){1,2}$/.test(hex)){
c = hex.substring(1).split('');
if(c.length === 3){
c = [c[0], c[0], c[1], c[1], c[2], c[2]];
}
c = '0x' + c.join('');
return [(c >> 16) & 255, (c >> 8) & 255, c & 255];
}
return [62, 56, 56]; // fallback slightly dark brown
};
const lcRgb = hexToRgb(lineColor);
highlightThreshold = Number(highlightThreshold);
saturation = Number(saturation);
paperTexture = Number(paperTexture);
// 1. Base Colors (Blur, Desaturate, Contrast)
const baseCanvas = document.createElement('canvas');
baseCanvas.width = width;
baseCanvas.height = height;
const baseCtx = baseCanvas.getContext('2d');
// Apply filters for color bleeding and saturation tweaks
baseCtx.filter = `blur(1.5px) saturate(${saturation}) contrast(1.1)`;
baseCtx.drawImage(originalImg, 0, 0);
const baseData = baseCtx.getImageData(0, 0, width, height);
const bPixels = baseData.data;
for(let i = 0; i < bPixels.length; i += 4) {
const a = bPixels[i+3];
if (a === 0) continue;
const r = bPixels[i];
const g = bPixels[i+1];
const b = bPixels[i+2];
const lum = 0.299 * r + 0.587 * g + 0.114 * b;
if (lum > highlightThreshold) {
// Emulate Ken Sugimori's signature large white painted highlights
const factor = Math.min((lum - highlightThreshold) / 15, 1);
bPixels[i] = r + (255 - r) * factor;
bPixels[i+1] = g + (255 - g) * factor;
bPixels[i+2] = b + (255 - b) * factor;
} else {
// Soft posterize to give distinct flat painted watercolor patches
const levels = 5;
const pr = Math.floor(r / (256/levels)) * (256/levels) + (256/levels)/2;
const pg = Math.floor(g / (256/levels)) * (256/levels) + (256/levels)/2;
const pb = Math.floor(b / (256/levels)) * (256/levels) + (256/levels)/2;
bPixels[i] = r * 0.4 + pr * 0.6;
bPixels[i+1] = g * 0.4 + pg * 0.6;
bPixels[i+2] = b * 0.4 + pb * 0.6;
}
}
baseCtx.putImageData(baseData, 0, 0);
// 2. Paper Texture Generation
if (paperTexture === 1) {
const noiseCanvas = document.createElement('canvas');
noiseCanvas.width = width;
noiseCanvas.height = height;
const nCtx = noiseCanvas.getContext('2d');
// Low-frequency watercolor blotches
const smallW = Math.max(1, Math.ceil(width / 4));
const smallH = Math.max(1, Math.ceil(height / 4));
const sCanvas = document.createElement('canvas');
sCanvas.width = smallW;
sCanvas.height = smallH;
const sCtx = sCanvas.getContext('2d');
const sData = sCtx.createImageData(smallW, smallH);
for(let i = 0; i < sData.data.length; i += 4) {
const val = 180 + Math.random() * 75;
sData.data[i] = val;
sData.data[i+1] = val;
sData.data[i+2] = val;
sData.data[i+3] = 255;
}
sCtx.putImageData(sData, 0, 0);
nCtx.filter = 'blur(4px)';
nCtx.drawImage(sCanvas, 0, 0, smallW, smallH, 0, 0, width, height);
// High-frequency paper grain
const gCanvas = document.createElement('canvas');
gCanvas.width = width;
gCanvas.height = height;
const gCtx = gCanvas.getContext('2d');
const gData = gCtx.createImageData(width, height);
for(let i = 0; i < gData.data.length; i += 4) {
const val = 230 + Math.random() * 25;
gData.data[i] = val;
gData.data[i+1] = val;
gData.data[i+2] = val;
gData.data[i+3] = 255;
}
gCtx.putImageData(gData, 0, 0);
nCtx.globalCompositeOperation = 'multiply';
nCtx.filter = 'none';
nCtx.drawImage(gCanvas, 0, 0);
// Mask Noise Canvas using BaseCanvas alpha to not texture fully transparent regions
const maskedNoiseCanvas = document.createElement('canvas');
maskedNoiseCanvas.width = width;
maskedNoiseCanvas.height = height;
const mnCtx = maskedNoiseCanvas.getContext('2d');
mnCtx.drawImage(noiseCanvas, 0, 0);
mnCtx.globalCompositeOperation = 'destination-in';
mnCtx.drawImage(baseCanvas, 0, 0);
// Apply the masked texture over the base layer
baseCtx.globalCompositeOperation = 'multiply';
baseCtx.drawImage(maskedNoiseCanvas, 0, 0);
baseCtx.globalCompositeOperation = 'source-over'; // Reset
}
// 3. Edge Detection (Custom Multi-Channel Sobel)
const edgeCanvas = document.createElement('canvas');
edgeCanvas.width = width;
edgeCanvas.height = height;
const edgeCtx = edgeCanvas.getContext('2d');
// Blur slightly so final detected edges have thickness, resembling ink strokes
edgeCtx.filter = 'blur(1.5px)';
edgeCtx.drawImage(originalImg, 0, 0);
const edgeImgData = edgeCtx.getImageData(0, 0, width, height);
const edges = new Uint8Array(width * height);
const gray = new Uint8Array(width * height);
const alphaMap = new Uint8Array(width * height);
for (let i = 0; i < width * height; i++) {
const idx = i * 4;
// Grayscale map
gray[i] = 0.299 * edgeImgData.data[idx] + 0.587 * edgeImgData.data[idx + 1] + 0.114 * edgeImgData.data[idx + 2];
// Alpha map
alphaMap[i] = edgeImgData.data[idx + 3];
}
const kernelX = [-1, 0, 1, -2, 0, 2, -1, 0, 1];
const kernelY = [-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 pxC = 0, pyC = 0;
let pxA = 0, pyA = 0;
for (let cy = -1; cy <= 1; cy++) {
for (let cx = -1; cx <= 1; cx++) {
const idx = ((y + cy) * width + (x + cx));
const wX = kernelX[(cy + 1) * 3 + (cx + 1)];
const wY = kernelY[(cy + 1) * 3 + (cx + 1)];
pxC += gray[idx] * wX;
pyC += gray[idx] * wY;
pxA += alphaMap[idx] * wX;
pyA += alphaMap[idx] * wY;
}
}
const magC = Math.sqrt(pxC * pxC + pyC * pyC);
const magA = Math.sqrt(pxA * pxA + pyA * pyA);
// Allow outlining colors or transparency transitions (guarantees sticker-like outline)
const mag = Math.max(magC, magA);
edges[y * width + x] = mag > 25 ? mag : 0;
}
}
const finalEdgeData = edgeCtx.createImageData(width, height);
for (let i = 0; i < width * height; i++) {
const mag = edges[i];
const idx = i * 4;
let alpha = (mag - 25) / (80 - 25);
if (alpha > 1) alpha = 1;
if (alpha < 0) alpha = 0;
finalEdgeData.data[idx] = lcRgb[0];
finalEdgeData.data[idx+1] = lcRgb[1];
finalEdgeData.data[idx+2] = lcRgb[2];
finalEdgeData.data[idx+3] = alpha * 255;
}
edgeCtx.putImageData(finalEdgeData, 0, 0);
// Filter edge canvas again to make the lines visually flow smoothly
const smoothEdgeCanvas = document.createElement('canvas');
smoothEdgeCanvas.width = width;
smoothEdgeCanvas.height = height;
const seCtx = smoothEdgeCanvas.getContext('2d');
seCtx.filter = 'blur(0.5px)';
seCtx.drawImage(edgeCanvas, 0, 0);
// Draw twice for punchy lineart that emulates slightly erratic inking
seCtx.drawImage(edgeCanvas, 0, 0);
// 4. Composite Everything Together
ctx.drawImage(baseCanvas, 0, 0);
ctx.drawImage(smoothEdgeCanvas, 0, 0);
return canvas;
}
Apply Changes