You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
cameraID = "CAM 01",
timestamp = "auto",
noiseAmount = 25,
distortion = 0.35,
compression = 0.25
) {
return new Promise((resolve) => {
// Output resolution: 720p HD surveillance footage
const width = 1280;
const height = 720;
// 1. Prepare offscreen canvas to scale and crop the original image
const offCanvas = document.createElement('canvas');
offCanvas.width = width;
offCanvas.height = height;
const offCtx = offCanvas.getContext('2d');
// Apply a slight greenish/desaturated filter simulating cheap sensor
offCtx.filter = 'saturate(0.55) contrast(1.15) brightness(0.95)';
// Calculate cover scaling, but slightly zoomed out (scale * 0.8) to simulate
// distance and leave room for strong wide-angle barrel distortion
const rawScale = Math.max(width / originalImg.width, height / originalImg.height);
const scale = rawScale * 0.85;
const w = originalImg.width * scale;
const h = originalImg.height * scale;
const x = (width - w) / 2;
const y = (height - h) / 2;
// Fill background with black to act as the camera barrel edge
offCtx.fillStyle = 'black';
offCtx.fillRect(0, 0, width, height);
// Draw image multiple times to simulate slight motion blur
offCtx.globalAlpha = 0.3;
offCtx.drawImage(originalImg, x - 3, y, w, h);
offCtx.drawImage(originalImg, x, y, w, h);
offCtx.globalAlpha = 0.4;
offCtx.drawImage(originalImg, x + 3, y, w, h);
offCtx.globalAlpha = 1.0;
// Extract pixel data to apply lens distortion and noise
const srcData = offCtx.getImageData(0, 0, width, height);
const sD = srcData.data;
// Create new canvas for distorted output
const outCanvas = document.createElement('canvas');
outCanvas.width = width;
outCanvas.height = height;
const outCtx = outCanvas.getContext('2d');
const outImageData = outCtx.createImageData(width, height);
const oD = outImageData.data;
const cx = width / 2;
const cy = height / 2;
const icx = 1 / cx;
const icy = 1 / cy;
// 2. Pixel Manipulation Loop: Perspective, Barrel Distortion, Chromatic Aberration, Noise
for (let py = 0; py < height; py++) {
const yn = (py - cy) * icy;
const pyw = py * width;
for (let px = 0; px < width; px++) {
const xn = (px - cx) * icx;
// Perspective shift: simulate camera mounted high pointing slightly down
// The top (yn < 0) is shrunk, pushing subjects back and widening the bottom.
const perspectiveTilt = 1.0 - 0.15 * yn;
const adjXn = xn * perspectiveTilt;
const adjYn = yn * (1.0 - 0.05 * yn);
// Barrel distortion (Fisheye wide-angle effect)
const r2 = adjXn * adjXn + adjYn * adjYn;
const distOffset = 1 + distortion * r2;
const srcX = adjXn * distOffset;
const srcY = adjYn * distOffset;
// Chromatic Aberration: sample R, G, B channels at slightly different scales
const rX = srcX * 0.988 * cx + cx;
const gX = srcX * 1.000 * cx + cx;
const bX = srcX * 1.012 * cx + cx;
const sY = srcY * cy + cy;
const outIdx = (pyw + px) * 4;
if (gX >= 0 && gX < width && sY >= 0 && sY < height) {
const syIdx = (sY | 0) * width;
const sxR = Math.max(0, Math.min(width - 1, rX | 0));
const sxG = gX | 0;
const sxB = Math.max(0, Math.min(width - 1, bX | 0));
const idxR = (syIdx + sxR) * 4;
const idxG = (syIdx + sxG) * 4;
const idxB = (syIdx + sxB) * 4;
const r = sD[idxR];
const g = sD[idxG + 1];
const b = sD[idxB + 2];
// Mild Sensor Noise
const noise = (Math.random() - 0.5) * noiseAmount;
oD[outIdx] = r + noise;
oD[outIdx + 1] = g + noise;
oD[outIdx + 2] = b + noise;
oD[outIdx + 3] = 255;
} else {
oD[outIdx + 3] = 255; // Leave as black outside the view
}
}
}
outCtx.putImageData(outImageData, 0, 0);
// 3. Post-processing Overlays: Vignette, Lighting, Interlacing, UI
// Radial Vignette
const gradient = outCtx.createRadialGradient(cx, cy, height * 0.3, cx, cy, width * 0.7);
gradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
gradient.addColorStop(1, 'rgba(0, 0, 0, 0.85)');
outCtx.fillStyle = gradient;
outCtx.fillRect(0, 0, width, height);
// Subtile dark green hardware cast
outCtx.fillStyle = 'rgba(20, 35, 25, 0.15)';
outCtx.fillRect(0, 0, width, height);
// Scanlines / Interlacing artifacts
outCtx.fillStyle = 'rgba(0, 0, 0, 0.1)';
for (let i = 0; i < height; i += 4) {
outCtx.fillRect(0, i, width, 1);
}
// Draw CCTV Interface
outCtx.font = 'bold 26px "Courier New", Courier, monospace';
outCtx.textBaseline = 'top';
const pad = 35;
// Format Timestamp
let dateStr = timestamp;
if (dateStr === "auto") {
const now = new Date();
const p = n => n.toString().padStart(2, '0');
dateStr = `${p(now.getDate())}-${p(now.getMonth()+1)}-${now.getFullYear()} ${p(now.getHours())}:${p(now.getMinutes())}:${p(now.getSeconds())}`;
}
// Text Styles
outCtx.fillStyle = 'rgba(255, 255, 255, 0.9)';
outCtx.shadowColor = 'black';
outCtx.shadowBlur = 4;
outCtx.shadowOffsetX = 2;
outCtx.shadowOffsetY = 2;
// Top Right: Timestamp
const tsWidth = outCtx.measureText(dateStr).width;
outCtx.fillText(dateStr, width - pad - tsWidth, pad);
// Bottom Left: Camera ID
outCtx.textBaseline = 'bottom';
outCtx.fillText(cameraID, pad, height - pad);
// Top Left: REC
outCtx.textBaseline = 'top';
outCtx.fillText("REC", pad + 25, pad);
outCtx.shadowColor = 'transparent';
outCtx.shadowOffsetX = 0;
outCtx.shadowOffsetY = 0;
outCtx.fillStyle = 'rgba(255, 0, 0, 0.85)';
outCtx.beginPath();
outCtx.arc(pad + 10, pad + 14, 6, 0, Math.PI * 2);
outCtx.fill();
// 4. Digital Compression: Simulate H.264/H.265 stream artifacts via low quality JPEG
// This naturally creates the blocky compression seen in security footage
const compressedDataUrl = outCanvas.toDataURL('image/jpeg', compression);
const finalImg = new Image();
finalImg.onload = () => {
const finalCanvas = document.createElement('canvas');
finalCanvas.width = width;
finalCanvas.height = height;
const finalCtx = finalCanvas.getContext('2d');
// Add a very slight blur to soften the hard digital edges mimicking a cheap lens
finalCtx.filter = 'blur(0.5px)';
finalCtx.drawImage(finalImg, 0, 0);
resolve(finalCanvas);
};
finalImg.src = compressedDataUrl;
});
}
Apply Changes