You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
pixelRes = 96,
blurSigma = 5,
noiseStd = 30,
darkenAlpha = 0.8,
darkenBeta = -30,
motionBlurSize = 15,
jpegQuality = 0.1,
scanLineSpacing = 4
) {
const w = originalImg.width;
const h = originalImg.height;
// Main canvas
const mainCanvas = document.createElement('canvas');
mainCanvas.width = w;
mainCanvas.height = h;
const ctx = mainCanvas.getContext('2d');
// 1. Pixelation: VERY low resolution & Upscale back (creates nearest-neighbor pixelation)
const smallCanvas = document.createElement('canvas');
smallCanvas.width = pixelRes;
smallCanvas.height = pixelRes;
const smallCtx = smallCanvas.getContext('2d');
smallCtx.drawImage(originalImg, 0, 0, pixelRes, pixelRes);
const upscaledCanvas = document.createElement('canvas');
upscaledCanvas.width = w;
upscaledCanvas.height = h;
const upCtx = upscaledCanvas.getContext('2d');
upCtx.imageSmoothingEnabled = false; // INTER_NEAREST equivalent
upCtx.drawImage(smallCanvas, 0, 0, w, h);
// 2. Strong Gaussian blur
ctx.filter = `blur(${blurSigma}px)`;
ctx.drawImage(upscaledCanvas, 0, 0);
ctx.filter = 'none';
// 3. Add heavy noise & Darken image (Simulate low light CCTV)
const imgData = ctx.getImageData(0, 0, w, h);
const data = imgData.data;
// Helper: Generate normally distributed noise (Box-Muller approximation)
function randomNormal() {
let u = 0, v = 0;
while (u === 0) u = Math.random();
while (v === 0) v = Math.random();
return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
}
for (let i = 0; i < data.length; i += 4) {
for (let c = 0; c < 3; c++) {
// Add normal distribution noise per color channel independent of each other
const noise = randomNormal() * noiseStd;
let val = data[i + c] + noise;
// Clamp intermediate noisy image to 0-255 before darken step
val = Math.max(0, Math.min(255, val));
// Simulate cv2.convertScaleAbs(img, alpha=0.8, beta=-30)
val = Math.abs(val * darkenAlpha + darkenBeta);
data[i + c] = val; // Uint8ClampedArray naturally clamps final value to 0-255
}
}
ctx.putImageData(imgData, 0, 0);
// 4. Motion blur (horizontal filter2D)
// Draw current context into a temporary buffer to use for sliding
const tempCanvas = document.createElement('canvas');
tempCanvas.width = w;
tempCanvas.height = h;
const tCtx = tempCanvas.getContext('2d');
tCtx.drawImage(mainCanvas, 0, 0);
ctx.clearRect(0, 0, w, h);
// We average images exactly by dynamically varying the alpha based on frame order (1/n)
let framesDrawn = 0;
let startOffset = -Math.floor((motionBlurSize - 1) / 2);
let endOffset = Math.floor(motionBlurSize / 2);
for (let offset = startOffset; offset <= endOffset; offset++) {
framesDrawn++;
ctx.globalAlpha = 1 / framesDrawn;
ctx.drawImage(tempCanvas, offset, 0);
}
ctx.globalAlpha = 1.0;
// 5. JPEG compression artifacts
// We encode the canvas to a low-quality JS Data URL JPEG and draw it back
const jpegUrl = mainCanvas.toDataURL('image/jpeg', jpegQuality);
const artifactImg = new Image();
artifactImg.src = jpegUrl;
await new Promise((resolve, reject) => {
artifactImg.onload = resolve;
artifactImg.onerror = reject;
});
ctx.drawImage(artifactImg, 0, 0);
// 6. Horizontal scan lines
ctx.fillStyle = 'rgba(0, 0, 0, 0.25)'; // Semi-transparent black lines
for (let y = 0; y < h; y += scanLineSpacing) {
ctx.fillRect(0, y, w, 1);
}
return mainCanvas;
}
Apply Changes