You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(
originalImg,
scanlineIntensity = 0.4,
noiseIntensity = 0.25,
grayscale = 0.8,
contrast = 1.5,
brightness = 1.2,
blurAmount = 1.0,
vhsTracking = 1,
rgbShift = 3,
timestampText = "1998-10-24 23:14:00",
vignetteAmount = 0.6,
tintColor = "none", // options: "none", "green", "blue", "amber"
pixelation = 2, // >= 1, lowers resolution for true 90s look
recDot = 1 // 1 for true, 0 for false
) {
// Parse parameters to ensure type safety
scanlineIntensity = Number(scanlineIntensity);
noiseIntensity = Number(noiseIntensity);
grayscale = Number(grayscale);
contrast = Number(contrast);
brightness = Number(brightness);
blurAmount = Number(blurAmount);
vhsTracking = Number(vhsTracking);
rgbShift = Math.floor(Number(rgbShift));
vignetteAmount = Number(vignetteAmount);
pixelation = Math.max(1, Math.floor(Number(pixelation)));
recDot = Number(recDot);
const w = originalImg.width;
const h = originalImg.height;
// Output Canvas setup
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
// Step 1: Pixelation pass via an off-screen canvas to downgrade quality
const tempCanvas = document.createElement('canvas');
tempCanvas.width = Math.ceil(w / pixelation);
tempCanvas.height = Math.ceil(h / pixelation);
const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(originalImg, 0, 0, tempCanvas.width, tempCanvas.height);
// Step 2: Draw the downscaled image scaled back up, applying standard post-processing filters
ctx.imageSmoothingEnabled = false; // Gives that jagged low-res aesthetic
ctx.filter = `blur(${blurAmount}px) brightness(${brightness}) contrast(${contrast}) grayscale(${grayscale})`;
ctx.drawImage(tempCanvas, 0, 0, w, h);
// Step 3: Draw Timestamp and REC marker
// We draw this BEFORE pixel manipulation so it gets the VHS glitch and scanlines baked into it
ctx.filter = 'none';
const fontSize = Math.max(12, Math.floor(h * 0.04));
ctx.font = `bold ${fontSize}px "Courier New", Courier, monospace`;
ctx.shadowColor = 'black';
ctx.shadowBlur = Math.max(2, fontSize * 0.2);
ctx.textAlign = 'left';
ctx.textBaseline = 'bottom';
if (timestampText) {
ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
ctx.fillText(timestampText, w * 0.05, h * 0.95);
}
if (recDot === 1) {
const dotX = w * 0.05 + fontSize * 0.4;
const dotY = h * 0.05 + fontSize * 0.4;
ctx.fillStyle = '#ff0000';
ctx.beginPath();
ctx.arc(dotX, dotY, fontSize * 0.4, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
ctx.textBaseline = 'middle';
ctx.fillText("REC", dotX + fontSize * 0.8, dotY);
}
// Step 4: Core pixel manipulations (VHS tracking, Noise, Chromatic Aberration, Scanlines, Vignette, Tint)
const srcImageData = ctx.getImageData(0, 0, w, h);
const srcData = srcImageData.data;
const dstImageData = ctx.createImageData(w, h);
const dstData = dstImageData.data;
const cx = w / 2;
const cy = h / 2;
const maxDist = Math.sqrt(cx * cx + cy * cy);
// Tint channel adjustments
let tr = 1, tg = 1, tb = 1;
if (tintColor === 'green') { tr = 0.5; tg = 1.3; tb = 0.5; }
else if (tintColor === 'blue') { tr = 0.5; tg = 0.6; tb = 1.4; }
else if (tintColor === 'amber') { tr = 1.2; tg = 0.9; tb = 0.3; }
for (let y = 0; y < h; y++) {
let trackOffset = 0;
// VHS Tracking tear artifacts near the bottom of the feed
if (vhsTracking === 1 && y > h * 0.85) {
trackOffset = Math.floor((Math.random() - 0.5) * (w * 0.02));
if (y > h * 0.9 && y < h * 0.92) {
// Severe tear band
trackOffset += Math.floor((Math.random() - 0.5) * (w * 0.06));
}
}
// Scanline logic + Interlacing
let scanFactor = 1;
if (scanlineIntensity > 0) {
// Darken alternate lines, and add a subtle vertical sine wave
const lineDarkness = y % 2 === 0 ? 0.3 : 0.0;
const waveDarkness = (Math.sin(y * Math.PI * 0.3) * 0.5 + 0.5) * 0.2;
scanFactor = 1 - (scanlineIntensity * (lineDarkness + waveDarkness));
}
for (let x = 0; x < w; x++) {
const dstIdx = (y * w + x) * 4;
let srcX = x + trackOffset;
if (srcX < 0) srcX = 0;
else if (srcX >= w) srcX = w - 1;
// Chromatic Aberration / RGB Glitch Offset
let rx = Math.max(0, srcX - rgbShift);
let gx = srcX;
let bx = Math.min(w - 1, srcX + rgbShift);
let rIdx = (y * w + rx) * 4;
let gIdx = (y * w + gx) * 4;
let bIdx = (y * w + bx) * 4;
let r = srcData[rIdx];
let g = srcData[gIdx + 1];
let b = srcData[bIdx + 2];
let a = srcData[gIdx + 3];
// Static Noise
if (noiseIntensity > 0) {
const noise = (Math.random() - 0.5) * 255 * noiseIntensity;
r += noise;
g += noise;
b += noise;
}
// CRT Color Tinting
r *= tr;
g *= tg;
b *= tb;
// Apply Scanlines
r *= scanFactor;
g *= scanFactor;
b *= scanFactor;
// Lens Vignette Extrapolation
if (vignetteAmount > 0) {
let dx = x - cx;
let dy = y - cy;
let dist = Math.sqrt(dx * dx + dy * dy);
let vFactor = 1 - (dist / maxDist) * vignetteAmount;
if (vFactor < 0) vFactor = 0;
r *= vFactor;
g *= vFactor;
b *= vFactor;
}
// Clamping and applying modifications to output pixels
dstData[dstIdx] = Math.max(0, Math.min(255, r));
dstData[dstIdx + 1] = Math.max(0, Math.min(255, g));
dstData[dstIdx + 2] = Math.max(0, Math.min(255, b));
dstData[dstIdx + 3] = a;
}
}
// Apply the manipulated pixels to the canvas buffer
ctx.putImageData(dstData, 0, 0);
return canvas;
}
Apply Changes