You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, cameraIdentifier = "CAM 04 - MAIN LOBBY", noiseLvl = "25", compressionBlockiness = "3", lensDistortion = "0.15") {
// Parse numerical parameters
const noiseLevel = parseFloat(noiseLvl) || 25;
const blockiness = parseFloat(compressionBlockiness) || 3;
const distK = parseFloat(lensDistortion) || 0.15;
// Create main canvas
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d", { willReadFrequently: true });
// Limit maximum dimension to 1280px to ensure good performance during pixel manipulation
const MAX_DIM = 1280;
let w = originalImg.width;
let h = originalImg.height;
if (w > MAX_DIM || h > MAX_DIM) {
const ratio = Math.min(MAX_DIM / w, MAX_DIM / h);
w = Math.floor(w * ratio);
h = Math.floor(h * ratio);
} else {
w = Math.floor(w);
h = Math.floor(h);
}
canvas.width = w;
canvas.height = h;
// 1. Draw original image with ghosting to simulate low-framerate motion blur
ctx.globalAlpha = 0.6;
ctx.drawImage(originalImg, 0, 0, w, h);
ctx.globalAlpha = 0.2;
ctx.drawImage(originalImg, 3, 0, w, h); // Shifted right
ctx.drawImage(originalImg, -2, 2, w, h); // Shifted bottom-left
ctx.globalAlpha = 1.0;
// 2. Pixel manipulation for surveillance filter effects
const imgData = ctx.getImageData(0, 0, w, h);
const data = imgData.data;
const outData = new Uint8ClampedArray(data.length);
const cx = w / 2;
const cy = h / 2;
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const destIdx = (y * w + x) * 4;
// Normalize coords for wide-angle barrel distortion
let nx = (x - cx) / cx;
let ny = (y - cy) / cy;
let r2 = nx * nx + ny * ny;
// Apply radial distortion factor
let f = 1 + distK * r2;
let sx = Math.floor(nx * f * cx + cx);
let sy = Math.floor(ny * f * cy + cy);
// Slight offset for red channel to simulate chromatic aberration of cheap lenses
let srx = Math.floor(nx * (f + 0.015) * cx + cx);
if (sx >= 0 && sx < w && sy >= 0 && sy < h) {
let srcIdx = (sy * w + sx) * 4;
let srcRIdx = (sy * w + Math.max(0, Math.min(w - 1, srx))) * 4;
let r = data[srcRIdx]; // Chromatic aberration Red
let g = data[srcIdx + 1];
let b = data[srcIdx + 2];
// Desaturate to mimic cheap CMOS sensors in low/indoor light
let gray = r * 0.299 + g * 0.587 + b * 0.114;
r = r * 0.25 + gray * 0.75;
g = g * 0.25 + gray * 0.75;
b = b * 0.25 + gray * 0.75;
// Color tint (slightly washed out fluorescent green/blue)
r *= 0.90;
g *= 1.02;
b *= 0.95;
// Add sensor noise
let noise = (Math.random() - 0.5) * noiseLevel;
r += noise;
g += noise;
b += noise;
// Subtle analog scanlines (darken every few rows)
if (y % 4 < 2) {
r *= 0.92;
g *= 0.92;
b *= 0.92;
}
outData[destIdx] = r;
outData[destIdx + 1] = g;
outData[destIdx + 2] = b;
outData[destIdx + 3] = 255;
} else {
// Out of bounds rendered as black for fisheye border effect
outData[destIdx] = 0;
outData[destIdx + 1] = 0;
outData[destIdx + 2] = 0;
outData[destIdx + 3] = 255;
}
}
}
// Apply modified pixels back to the main canvas
data.set(outData);
ctx.putImageData(imgData, 0, 0);
// 3. Add H.264 artifacting/blockiness by harsh downscaling and upscaling
if (blockiness > 1.0) {
const smallCanvas = document.createElement("canvas");
smallCanvas.width = Math.max(1, Math.floor(w / blockiness));
smallCanvas.height = Math.max(1, Math.floor(h / blockiness));
const sCtx = smallCanvas.getContext("2d");
// Draw normal to small canvas
sCtx.imageSmoothingEnabled = true;
sCtx.drawImage(canvas, 0, 0, smallCanvas.width, smallCanvas.height);
// Draw scaled up to main canvas using nearest-neighbor (pixelated)
ctx.imageSmoothingEnabled = false;
ctx.drawImage(smallCanvas, 0, 0, w, h);
ctx.imageSmoothingEnabled = true;
}
// 4. Heavy vignette overlay (darkening corners)
const vignette = ctx.createRadialGradient(cx, cy, Math.min(cx, cy) * 0.3, cx, cy, Math.max(cx, cy) * 1.1);
vignette.addColorStop(0, "rgba(0,0,0,0)");
vignette.addColorStop(1, "rgba(0,0,0,0.85)");
ctx.fillStyle = vignette;
ctx.fillRect(0, 0, w, h);
// 5. OSD (On-Screen Display) overlays for realism
const pad = (num) => num.toString().padStart(2, "0");
const now = new Date();
const timestampStr = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
const fontSize = Math.max(14, Math.floor(h * 0.035));
ctx.font = `bold ${fontSize}px "Courier New", Courier, monospace`;
ctx.textBaseline = "top";
// Helper to draw realistic typical CCTV text (white with black outline)
function drawOSDText(text, x, y, align = "left") {
ctx.textAlign = align;
ctx.lineWidth = Math.max(2, Math.floor(fontSize * 0.15));
ctx.strokeStyle = "rgba(0, 0, 0, 0.8)";
ctx.strokeText(text, x, y);
ctx.fillStyle = "rgba(245, 245, 245, 0.95)";
ctx.fillText(text, x, y);
}
// Top-left: Timestamp
drawOSDText(timestampStr, Math.floor(w * 0.02), Math.floor(h * 0.02), "left");
// Top-right: Camera Name / Location
drawOSDText(cameraIdentifier, Math.floor(w * 0.98), Math.floor(h * 0.02), "right");
// Bottom-left: Recording Indicator (fake blinking)
if (Math.random() > 0.3) {
let recX = Math.floor(w * 0.02);
let recY = Math.floor(h * 0.98) - fontSize;
ctx.textAlign = "left";
ctx.lineWidth = Math.max(2, Math.floor(fontSize * 0.15));
// Draw red dot
ctx.beginPath();
let dotCenterY = recY + (fontSize * 0.5);
ctx.arc(recX + (fontSize * 0.4), dotCenterY, fontSize * 0.35, 0, Math.PI * 2);
ctx.fillStyle = "rgba(220, 20, 20, 0.95)";
ctx.fill();
ctx.strokeStyle = "rgba(0, 0, 0, 0.8)";
ctx.stroke();
// Draw "REC" text
drawOSDText("REC", recX + fontSize * 1.1, recY, "left");
}
return canvas;
}
Apply Changes