You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, timestamp = '', camId = 'CAM 01', showRec = 1, noiseAmount = 0.1, scanlineOpacity = 0.1, vignetteAmount = 0.5, saturation = 0) {
// 1. Setup Canvas
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const {
width,
height
} = originalImg;
canvas.width = width;
canvas.height = height;
// 2. Dynamically load a monospaced font for the overlay text.
// This is async, so the function must be async.
try {
const font = new FontFace('CCTVFont', 'url(https://fonts.gstatic.com/s/robotomono/v22/L0x5DF4xlVMF-BfR8bXMIjhGq3-cXbKDO1w.woff2)');
await font.load();
document.fonts.add(font);
} catch (e) {
console.error("CCTV font could not be loaded. A default monospace font will be used.", e);
}
// 3. Draw the base image with color saturation adjustment.
// saturation=0 creates a grayscale image.
ctx.filter = `saturate(${saturation})`;
ctx.drawImage(originalImg, 0, 0, width, height);
ctx.filter = 'none'; // Reset filter for subsequent drawings
// 4. Add noise to the image.
if (noiseAmount > 0) {
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
const noiseFactor = 255 * Math.max(0, Math.min(1, noiseAmount));
for (let i = 0; i < data.length; i += 4) {
// Add the same random noise to R, G, and B channels
const noise = (Math.random() - 0.5) * noiseFactor;
data[i] = Math.max(0, Math.min(255, data[i] + noise));
data[i + 1] = Math.max(0, Math.min(255, data[i + 1] + noise));
data[i + 2] = Math.max(0, Math.min(255, data[i + 2] + noise));
}
ctx.putImageData(imageData, 0, 0);
}
// 5. Add horizontal scan lines.
if (scanlineOpacity > 0) {
ctx.fillStyle = `rgba(0, 0, 0, ${scanlineOpacity})`;
for (let i = 0; i < height; i += 3) {
ctx.fillRect(0, i, width, 1);
}
}
// 6. Add a vignette effect (darker corners).
if (vignetteAmount > 0) {
const outerRadius = Math.sqrt(Math.pow(width / 2, 2) + Math.pow(height / 2, 2));
const gradient = ctx.createRadialGradient(
width / 2, height / 2, outerRadius * (1 - vignetteAmount),
width / 2, height / 2, outerRadius
);
gradient.addColorStop(0, 'rgba(0,0,0,0)');
// A darker, more opaque black at the very edge.
gradient.addColorStop(1, 'rgba(0,0,0,0.8)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);
}
// 7. Add text overlays (CAM ID, REC, Timestamp).
const fontSize = Math.max(12, Math.round(height / 35));
const padding = fontSize * 0.5;
ctx.font = `${fontSize}px CCTVFont, monospace`;
ctx.fillStyle = 'rgba(255, 255, 255, 0.75)';
ctx.textBaseline = 'top';
// Top-left: Camera ID
if (camId) {
ctx.textAlign = 'left';
ctx.fillText(camId, padding, padding);
}
// Top-right: REC indicator
if (Number(showRec) === 1) {
ctx.textAlign = 'right';
const recText = "REC";
const textMetrics = ctx.measureText(recText);
const recTextX = width - padding;
const recTextY = padding;
ctx.fillStyle = 'rgba(255, 255, 255, 0.75)';
ctx.fillText(recText, recTextX, recTextY);
const recCircleRadius = fontSize / 3;
// Position circle to the left of the "REC" text
const circleX = recTextX - textMetrics.width - recCircleRadius - (padding / 2);
const circleY = recTextY + (fontSize / 2);
ctx.fillStyle = 'rgba(255, 0, 0, 0.9)';
ctx.beginPath();
ctx.arc(circleX, circleY, recCircleRadius, 0, 2 * Math.PI);
ctx.fill();
}
// Bottom-right: Timestamp
let ts = timestamp;
if (!ts) {
const now = new Date();
const YYYY = now.getFullYear();
const MM = String(now.getMonth() + 1).padStart(2, '0');
const DD = String(now.getDate()).padStart(2, '0');
const hh = String(now.getHours()).padStart(2, '0');
const mm = String(now.getMinutes()).padStart(2, '0');
const ss = String(now.getSeconds()).padStart(2, '0');
ts = `${YYYY}-${MM}-${DD} ${hh}:${mm}:${ss}`;
}
if (ts) {
ctx.textAlign = 'right';
ctx.textBaseline = 'bottom';
ctx.fillStyle = 'rgba(255, 255, 255, 0.75)';
ctx.fillText(ts, width - padding, height - padding);
}
// 8. Return the final canvas.
return canvas;
}
Apply Changes