You can edit the below JavaScript code to customize the image tool.
/**
* Processes an input image to make it look like it was taken by a realistic CCTV camera.
*
* @param {HTMLImageElement} originalImg - The original source image to process.
* @param {number|string} resolutionWidth - The width to scale down to (simulates low-res sensor).
* @param {number|string} resolutionHeight - The height to scale down to (simulates low-res sensor).
* @param {number|string} grayscaleAmount - 0.0 to 1.0 (typically CCTV is heavily desaturated or B&W).
* @param {number|string} contrastAmount - 0.0 to 2.0 (CCTV usually has blown out or harsh contrast).
* @param {number|string} brightnessAmount - 0.0 to 2.0 (Simulates exposure).
* @param {number|string} blurAmount - Blur radius in pixels (low-quality lens focus).
* @param {number|string} noiseAmount - Intensity of random sensor noise (0-255).
* @param {number|string} jpegQuality - 0.0 to 1.0 (forces heavy compression artifacts, e.g. 0.15).
* @param {number|string} motionBlurLength - Number of pixels to smear for simulated motion blur.
* @param {number|string} motionBlurAngle - Angle in degrees for the motion blur direction.
* @param {number|string} addOverlay - 1 to add fake camera date/time text, 0 to disable.
* @returns {HTMLCanvasElement} A canvas element containing the processed image.
*/
async function processImage(
originalImg,
resolutionWidth = 640,
resolutionHeight = 480,
grayscaleAmount = 0.85,
contrastAmount = 1.3,
brightnessAmount = 0.9,
blurAmount = 1.2,
noiseAmount = 25,
jpegQuality = 0.15,
motionBlurLength = 0,
motionBlurAngle = 0,
addOverlay = 1
) {
// Parse all parameters from strings to numbers to ensure math works correctly
const width = Number(resolutionWidth);
const height = Number(resolutionHeight);
const grayscale = Number(grayscaleAmount);
const contrast = Number(contrastAmount);
const brightness = Number(brightnessAmount);
const blur = Number(blurAmount);
const noise = Number(noiseAmount);
const jQuality = Number(jpegQuality);
const mBlurLen = Number(motionBlurLength);
const mBlurAng = Number(motionBlurAngle);
const overlay = Number(addOverlay);
// Create a new canvas element that will hold the processing steps
const canvas = document.createElement('canvas');
// Set the canvas width to the target low resolution
canvas.width = width;
// Set the canvas height to the target low resolution
canvas.height = height;
// Get the 2D drawing context from the canvas
const ctx = canvas.getContext('2d', { willReadFrequently: true });
// Apply multiple CSS-like filters: Grayscale, Contrast, Brightness, and Gaussian Blur
ctx.filter = `grayscale(${grayscale * 100}%) contrast(${contrast * 100}%) brightness(${brightness * 100}%) blur(${blur}px)`;
// Check if motion blur is requested (length > 0)
if (mBlurLen > 0) {
// Calculate transparency so overlapping frames add up to full opacity
ctx.globalAlpha = 1.0 / (mBlurLen + 1);
// Convert the blur angle from degrees to radians
const angleRad = mBlurAng * Math.PI / 180;
// Loop 'mBlurLen' times to draw shifted frames
for (let i = 0; i <= mBlurLen; i++) {
// Calculate X offset using cosine
const offsetX = Math.cos(angleRad) * i;
// Calculate Y offset using sine
const offsetY = Math.sin(angleRad) * i;
// Draw the image at the shifted position to construct motion blur
ctx.drawImage(originalImg, offsetX, offsetY, width, height);
}
// Reset the global alpha back to 1.0 (fully opaque)
ctx.globalAlpha = 1.0;
} else {
// If no motion blur, simply draw the image stretched to the low-res canvas
ctx.drawImage(originalImg, 0, 0, width, height);
}
// Reset the context filter back to none so future items (noise, text) aren't blurred
ctx.filter = 'none';
// Check if sensor noise should be added
if (noise > 0) {
// Retrieve the pixel data of the entire canvas
const imgData = ctx.getImageData(0, 0, width, height);
// Reference the underlying 1D array of RGBA color values
const data = imgData.data;
// Loop through each pixel (4 values per pixel: R, G, B, A)
for (let i = 0; i < data.length; i += 4) {
// Generate a random noise value between -noise and +noise
const n = (Math.random() - 0.5) * 2 * noise;
// Apply noise to Red channel and clamp between 0-255
data[i] = Math.max(0, Math.min(255, data[i] + n));
// Apply noise to Green channel and clamp between 0-255
data[i+1] = Math.max(0, Math.min(255, data[i+1] + n));
// Apply noise to Blue channel and clamp between 0-255
data[i+2] = Math.max(0, Math.min(255, data[i+2] + n));
// Alpha channel (data[i+3]) is left unchanged
}
// Put the modified noisy pixel data back onto the canvas
ctx.putImageData(imgData, 0, 0);
}
// Apply JPEG compression artifacts if quality is less than 1.0
if (jQuality < 1.0) {
// Convert the current canvas state into a low-quality JPEG Data URL string
const jpegDataUrl = canvas.toDataURL('image/jpeg', jQuality);
// Create a temporary Image object to load the compressed version
const tempImg = new Image();
// Set the source to the compressed JPEG string
tempImg.src = jpegDataUrl;
// Create a Promise to pause execution until the image finishes loading
await new Promise(resolve => {
// Resolve promise when load is successful
tempImg.onload = resolve;
// Resolve promise even if it errors to avoid freezing
tempImg.onerror = resolve;
});
// Clear the canvas to prep for drawing the artifact-heavy image
ctx.clearRect(0, 0, width, height);
// Draw the highly compressed JPEG image back over the canvas
ctx.drawImage(tempImg, 0, 0, width, height);
}
// Add faint scanlines giving it an old interlaced monitor feel
ctx.fillStyle = 'rgba(0, 0, 0, 0.15)'; // Semi-transparent black
// Loop through the canvas vertically, skipping 3 pixels at a time
for (let y = 0; y < height; y += 3) {
// Draw a 1-pixel high horizontal line across the canvas width
ctx.fillRect(0, y, width, 1);
}
// Check if the timestamp and camera ID overlay is requested
if (overlay === 1) {
// Calculate a responsive font size based on the canvas height (min 12px)
const fontSize = Math.max(12, Math.floor(height * 0.04));
// Set the font style to monospace to look like technical output
ctx.font = `bold ${fontSize}px "Courier New", Courier, monospace`;
// Align text to start drawing from the given X coordinate going right
ctx.textAlign = 'left';
// Align text baseline to top so it anchors safely at the top edge
ctx.textBaseline = 'top';
// Generate the current date and time
const now = new Date();
// Format the date/time string strictly (YYYY-MM-DD HH:MM:SS)
const dateString = now.toISOString().replace('T', ' ').substring(0, 19);
// Create a fake camera name like CAM-04 with the timestamp
const camText = `CAM-${Math.floor(Math.random() * 9 + 1).toString().padStart(2, '0')} ${dateString}`;
// Set the outline thickness for the text so it can be seen uniformly
ctx.lineWidth = 3;
// Set stroke color to black
ctx.strokeStyle = '#000000';
// Set fill color to white
ctx.fillStyle = '#FFFFFF';
// Draw the black outline of the top-left camera text
ctx.strokeText(camText, 10, 10);
// Draw the white inside of the top-left camera text
ctx.fillText(camText, 10, 10);
// Switch text alignment to right so the right-side text anchor works
ctx.textAlign = 'right';
// Define standard recording indicator text
const recText = 'REC ';
// Draw the black outline of 'REC' in the top-right corner
ctx.strokeText(recText, width - 25, 10);
// Draw the white inside of 'REC' in the top-right corner
ctx.fillText(recText, width - 25, 10);
// Change fill color to red to draw the REC circle
ctx.fillStyle = '#FF0000';
// Start a new vector path for the recording dot
ctx.beginPath();
// Define the X position for the center of the recording dot
const dotX = width - 15;
// Define the Y position for the center of the recording dot
const dotY = 10 + (fontSize / 2);
// Map out a full circle for the dot
ctx.arc(dotX, dotY, fontSize / 3, 0, Math.PI * 2);
// Fill the circle with the red color
ctx.fill();
// Stroke the black outline around the circle for extra contrast
ctx.stroke();
}
// Return the final processed Canvas element containing the realistic CCTV image
return canvas;
}
Free Image Tool Creator
Can't find the image tool you're looking for? Create one based on your own needs now!
This tool transforms your standard photos into realistic CCTV-style security footage. It simulates the visual characteristics of surveillance cameras by adjusting resolution, applying grayscale filters, and manipulating contrast and brightness. Additionally, it can add technical effects such as sensor noise, motion blur, JPEG compression artifacts, and scanlines. You can also include a digital overlay featuring a timestamp and a recording indicator to complete the look. This tool is ideal for creative projects, film production, or adding a suspenseful atmosphere to digital content.