You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, dotSizeParam = "auto", edgeToleranceParam = "60", colorLevelsParam = "4", misregistrationParam = "auto") {
const width = originalImg.width;
const height = originalImg.height;
// Create a working canvas
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
// Fill with white to remove transparency correctly
ctx.fillStyle = '#FFFFFF';
ctx.fillRect(0, 0, width, height);
ctx.drawImage(originalImg, 0, 0);
const srcData = ctx.getImageData(0, 0, width, height).data;
const outImgData = ctx.createImageData(width, height);
const outData = outImgData.data;
// Parameter parsing and auto-scaling
const baseScale = Math.max(width, height) / 1000;
const autoDotSize = Math.max(3, Math.round(6 * baseScale));
const dotSize = dotSizeParam === "auto" ? autoDotSize : (parseFloat(dotSizeParam) || autoDotSize);
const edgeTolerance = parseFloat(edgeToleranceParam) || 60;
const levels = Math.max(2, parseInt(colorLevelsParam) || 4);
const autoMisreg = Math.max(1, Math.round(3 * baseScale));
const misreg = misregistrationParam === "auto" ? autoMisreg : (parseFloat(misregistrationParam) || autoMisreg);
// 1. Calculate base Luma for fast edge detection
const luma = new Int32Array(width * height);
for (let i = 0; i < width * height; i++) {
luma[i] = srcData[i * 4] * 0.299 + srcData[i * 4 + 1] * 0.587 + srcData[i * 4 + 2] * 0.114;
}
// 2. Simple Box Blur on Luma to reduce noise before Sobel
const blurredLuma = new Int32Array(width * height);
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
const idx = y * width + x;
blurredLuma[idx] = (
luma[idx - width - 1] + luma[idx - width] + luma[idx - width + 1] +
luma[idx - 1] + luma[idx] + luma[idx + 1] +
luma[idx + width - 1] + luma[idx + width] + luma[idx + width + 1]
) / 9;
}
}
// 3. Sobel Edge Detection
const edges = new Uint8Array(width * height);
for (let y = 2; y < height - 2; y++) {
for (let x = 2; x < width - 2; x++) {
const idx = y * width + x;
const p00 = blurredLuma[idx - width - 1]; const p01 = blurredLuma[idx - width]; const p02 = blurredLuma[idx - width + 1];
const p10 = blurredLuma[idx - 1]; const p12 = blurredLuma[idx + 1];
const p20 = blurredLuma[idx + width - 1]; const p21 = blurredLuma[idx + width]; const p22 = blurredLuma[idx + width + 1];
const gx = (p02 + 2 * p12 + p22) - (p00 + 2 * p10 + p20);
const gy = (p20 + 2 * p21 + p22) - (p00 + 2 * p01 + p02);
if (Math.abs(gx) + Math.abs(gy) > edgeTolerance) {
edges[idx] = 1;
}
}
}
// 4. Dilate edges to create thick Comic-style Inks
const thickEdges = new Uint8Array(width * height);
for (let y = 2; y < height - 2; y++) {
for (let x = 2; x < width - 2; x++) {
const idx = y * width + x;
if (edges[idx] === 1) {
// Cross + Diagonal dilation for smooth, thick shapes
thickEdges[idx] = 1;
thickEdges[idx - 1] = 1;
thickEdges[idx + 1] = 1;
thickEdges[idx - width] = 1;
thickEdges[idx + width] = 1;
thickEdges[idx - width - 1] = 1;
thickEdges[idx - width + 1] = 1;
thickEdges[idx + width - 1] = 1;
thickEdges[idx + width + 1] = 1;
}
}
}
// 5. CMYK Halftone Rendering
const freq = Math.PI * 2 / dotSize;
// Rotation angles for halftone screens
const rC = 15 * Math.PI / 180, cosC = Math.cos(rC), sinC = Math.sin(rC);
const rM = 75 * Math.PI / 180, cosM = Math.cos(rM), sinM = Math.sin(rM);
const rY = 0 * Math.PI / 180, cosY = Math.cos(rY), sinY = Math.sin(rY);
const rK = 45 * Math.PI / 180, cosK = Math.cos(rK), sinK = Math.sin(rK);
// WW2 Era Vintage Ink Palette (Subdued)
const pPaper = [240, 235, 215];
const iCyan = [0, 140, 200];
const iMag = [200, 60, 100];
const iYel = [230, 200, 30];
const iBlack = [30, 25, 30];
// Offsets to simulate misregistration during printing
const cX = misreg, cY = misreg;
const mX = -misreg, mY = misreg;
const yX = misreg, yY = -misreg;
function getPosterizedColor(ox, oy, baseX, baseY) {
let cx = baseX + ox;
let cy = baseY + oy;
cx = Math.max(0, Math.min(width - 1, cx));
cy = Math.max(0, Math.min(height - 1, cy));
const idx = (cy * width + cx) * 4;
let cR = srcData[idx];
let cG = srcData[idx + 1];
let cB = srcData[idx + 2];
// Contrast boost to make primary colors pop more
cR = Math.min(255, Math.max(0, (cR - 128) * 1.3 + 128));
cG = Math.min(255, Math.max(0, (cG - 128) * 1.3 + 128));
cB = Math.min(255, Math.max(0, (cB - 128) * 1.3 + 128));
// Quantization (Posterization)
const step = 255 / (levels - 1);
cR = Math.round(cR / step) * step;
cG = Math.round(cG / step) * step;
cB = Math.round(cB / step) * step;
return [cR, cG, cB];
}
const hw = width / 2;
const hh = height / 2;
const maxDistSq = hw * hw + hh * hh;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const idx = y * width + x;
const outIdx = idx * 4;
// Apply vignette and film grain
const distSq = (x - hw) * (x - hw) + (y - hh) * (y - hh);
const vignette = 1.0 - (distSq / maxDistSq) * 0.35;
const noise = (Math.random() * 0.08) + 0.96;
if (thickEdges[idx]) {
outData[outIdx] = iBlack[0] * vignette * noise;
outData[outIdx + 1] = iBlack[1] * vignette * noise;
outData[outIdx + 2] = iBlack[2] * vignette * noise;
outData[outIdx + 3] = 255;
continue;
}
// Extract channels from misregistered locations
const colC = getPosterizedColor(cX, cY, x, y);
const valC = 1 - colC[0] / 255;
const colM = getPosterizedColor(mX, mY, x, y);
const valM = 1 - colM[1] / 255;
const colY = getPosterizedColor(yX, yY, x, y);
const valY = 1 - colY[2] / 255;
const colK = getPosterizedColor(0, 0, x, y);
const kBase = Math.min(1 - colK[0] / 255, 1 - colK[1] / 255, 1 - colK[2] / 255);
const kInk = kBase * 0.8; // 80% Gray Component Replacement
// Adjusted CMY
const adjC = Math.max(0, valC - kInk);
const adjM = Math.max(0, valM - kInk);
const adjY = Math.max(0, valY - kInk);
// Sine wave threshold halftoning
const tc = (Math.sin((x * cosC - y * sinC) * freq) + Math.sin((x * sinC + y * cosC) * freq)) / 4 + 0.5;
const tm = (Math.sin((x * cosM - y * sinM) * freq) + Math.sin((x * sinM + y * cosM) * freq)) / 4 + 0.5;
const ty = (Math.sin((x * cosY - y * sinY) * freq) + Math.sin((x * sinY + y * cosY) * freq)) / 4 + 0.5;
const tk = (Math.sin((x * cosK - y * sinK) * freq) + Math.sin((x * sinK + y * cosK) * freq)) / 4 + 0.5;
let resR = pPaper[0], resG = pPaper[1], resB = pPaper[2];
// Subtractive synthesis
if (adjC > tc) { resR = resR * iCyan[0] / 255; resG = resG * iCyan[1] / 255; resB = resB * iCyan[2] / 255; }
if (adjM > tm) { resR = resR * iMag[0] / 255; resG = resG * iMag[1] / 255; resB = resB * iMag[2] / 255; }
if (adjY > ty) { resR = resR * iYel[0] / 255; resG = resG * iYel[1] / 255; resB = resB * iYel[2] / 255; }
if (kInk > tk) { resR = resR * iBlack[0] / 255; resG = resG * iBlack[1] / 255; resB = resB * iBlack[2] / 255; }
outData[outIdx] = Math.min(255, Math.max(0, resR * vignette * noise));
outData[outIdx + 1] = Math.min(255, Math.max(0, resG * vignette * noise));
outData[outIdx + 2] = Math.min(255, Math.max(0, resB * vignette * noise));
outData[outIdx + 3] = 255;
}
}
ctx.putImageData(outImgData, 0, 0);
return canvas;
}
Apply Changes