You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, noiseLevel = 0.15, scanlineAmount = 0.2, glitchCount = 7) {
// Convert parameters to numbers just in case they are passed as strings
const noise = Number(noiseLevel) * 255;
const scanlineAlpha = Number(scanlineAmount);
const glitches = Number(glitchCount);
const canvas = document.createElement('canvas');
const width = originalImg.width;
const height = originalImg.height;
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
// 1. Initial Grayscale and Contrast pass (HIFI Mono look)
// Enhancing contrast and brightness slightly simulates cheap analog dubs
ctx.filter = 'grayscale(100%) contrast(1.4) brightness(1.1)';
ctx.drawImage(originalImg, 0, 0);
ctx.filter = 'none';
// 2. Luma Smear / Horizontal Ghosting (Analog bleed effect)
ctx.globalAlpha = 0.35;
// Shift slightly right to create a trailing ghost image
ctx.drawImage(canvas, Math.max(3, width * 0.005), 0);
ctx.globalAlpha = 1.0;
// 3. Tracking errors / Glitches (Displace horizontal strips)
const maxShift = width * 0.04;
for (let i = 0; i < glitches; i++) {
const sliceY = Math.floor(Math.random() * height);
const sliceHeight = Math.floor(Math.random() * (height * 0.04) + 2);
const shiftX = Math.floor((Math.random() - 0.5) * maxShift);
if (sliceY + sliceHeight <= height) {
const sliceData = ctx.getImageData(0, sliceY, width, sliceHeight);
// Draw a black rectangle underneath to simulate signal loss gap
ctx.fillStyle = '#111';
ctx.fillRect(0, sliceY, width, sliceHeight);
// Place the shifted slice back
ctx.putImageData(sliceData, shiftX, sliceY);
}
}
// 4. Pixel Operations: Noise & Scanlines
const imgData = ctx.getImageData(0, 0, width, height);
const data = imgData.data;
for (let y = 0; y < height; y++) {
// Darken certain rows to simulate CRT scanlines
const isScanline = (y % 3 === 0);
const scanlineFactor = isScanline ? (1 - scanlineAlpha) : 1;
for (let x = 0; x < width; x++) {
const index = (y * width + x) * 4;
// Generate noise (same value for R, G, B keeps it strictly monochromatic)
const n = (Math.random() - 0.5) * noise;
let r = data[index] + n;
let g = data[index + 1] + n;
let b = data[index + 2] + n;
// Apply scanline darkening
r *= scanlineFactor;
g *= scanlineFactor;
b *= scanlineFactor;
// Clamp and update pixels
data[index] = Math.min(255, Math.max(0, r));
data[index + 1] = Math.min(255, Math.max(0, g));
data[index + 2] = Math.min(255, Math.max(0, b));
// Alpha channel data[index + 3] remains untouched
}
}
// Apply modified pixel data back to canvas
ctx.putImageData(imgData, 0, 0);
// 5. Vignette (Darkened screen edges)
const gradient = ctx.createRadialGradient(
width / 2, height / 2, width * 0.4,
width / 2, height / 2, Math.max(width, height) * 0.75
);
gradient.addColorStop(0, 'rgba(0,0,0,0)');
gradient.addColorStop(1, 'rgba(0,0,0,0.65)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);
// 6. VCR On-Screen Display (OSD) Overlay
ctx.fillStyle = 'rgba(255, 255, 255, 0.85)';
const fontSize = Math.max(16, Math.floor(height * 0.05));
ctx.font = `bold ${fontSize}px "Courier New", Courier, monospace`;
// Add text shadow for CRT glow
ctx.shadowColor = "rgba(0, 0, 0, 0.8)";
ctx.shadowBlur = 4;
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
const marginX = width * 0.05;
const marginY = height * 0.08;
ctx.fillText("PLAY ►", marginX, marginY);
ctx.fillText("MONO", width - marginX - ctx.measureText("MONO").width, marginY);
// Date/Time indicator at bottom left
const now = new Date();
const timeStr = `${String(now.getHours() % 12 || 12).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')} ${now.getHours() >= 12 ? 'PM' : 'AM'}`;
ctx.fillText(timeStr, marginX, height - marginY);
return canvas;
}
Apply Changes