You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, chromaticAberration = "5", noiseLevel = "0.08", scanlineWidth = "2", showOSD = "true") {
const canvas = document.createElement('canvas');
const width = originalImg.width;
const height = originalImg.height;
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
// Draw the base image
ctx.drawImage(originalImg, 0, 0);
// Get image data to apply VHS distortion, chromatic aberration, and noise manually
const imgData = ctx.getImageData(0, 0, width, height);
const data = imgData.data;
const outData = new Uint8ClampedArray(data.length);
const aberration = Math.round(Number(chromaticAberration) * (width / 800));
const noise = Number(noiseLevel) * 255;
const sWidth = Math.max(1, Math.round(Number(scanlineWidth) * (height / 800)));
// Traverse pixels
for (let y = 0; y < height; y++) {
let trackingShift = 0;
let noiseMultiplier = 1;
// Bottom tracking noise zone (typical for old VHS tapes)
if (y > height * 0.92 && y < height * 0.97) {
trackingShift = (Math.random() - 0.5) * (width * 0.03);
noiseMultiplier = 4;
}
// Random occasional glitch lines elsewhere
else if (Math.random() > 0.99) {
trackingShift = (Math.random() - 0.5) * (width * 0.02);
noiseMultiplier = 2;
}
// Gentle wavy distortion across the screen
const wave = Math.sin(y / height * Math.PI * 10) * width * 0.002;
const shiftX = trackingShift + wave;
for (let x = 0; x < width; x++) {
const i = (y * width + x) * 4;
// Calculate shifted positions for RGB channels
let rX = Math.round(Math.min(Math.max(x + aberration + shiftX, 0), width - 1));
let gX = Math.round(Math.min(Math.max(x + shiftX, 0), width - 1));
let bX = Math.round(Math.min(Math.max(x - aberration + shiftX, 0), width - 1));
const rI = (y * width + rX) * 4;
const gI = (y * width + gX) * 4;
const bI = (y * width + bX) * 4;
// Apply noise
let n = (Math.random() - 0.5) * noise * noiseMultiplier;
outData[i] = Math.min(Math.max(data[rI] + n, 0), 255); // R
outData[i + 1] = Math.min(Math.max(data[gI + 1] + n, 0), 255); // G
outData[i + 2] = Math.min(Math.max(data[bI + 2] + n, 0), 255); // B
outData[i + 3] = data[gI + 3]; // Alpha
}
}
// Put the manipulated data back
const newImgData = new ImageData(outData, width, height);
ctx.putImageData(newImgData, 0, 0);
// Soften and slightly offset the image to mimic VHS tape color ghosting
ctx.globalAlpha = 0.4;
ctx.filter = `blur(${Math.max(1, width * 0.001)}px)`;
ctx.drawImage(canvas, width * 0.005, 0);
ctx.filter = 'none';
ctx.globalAlpha = 1.0;
// Add interlacing scanlines
ctx.fillStyle = 'rgba(0, 0, 0, 0.15)';
for (let y = 0; y < height; y += sWidth * 2) {
ctx.fillRect(0, y, width, sWidth);
}
// Desaturate slightly to mimic old tape color fade
ctx.fillStyle = 'rgba(128, 128, 128, 0.2)';
ctx.globalCompositeOperation = 'saturation';
ctx.fillRect(0, 0, width, height);
// Edge darkening (vignette effect)
ctx.globalCompositeOperation = 'source-over';
const gradient = ctx.createRadialGradient(width/2, height/2, width*0.4, width/2, height/2, width*0.8);
gradient.addColorStop(0, 'rgba(0,0,0,0)');
gradient.addColorStop(1, 'rgba(0,0,0,0.6)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);
// Overlay VCR OSD (On-Screen Display)
if (String(showOSD).toLowerCase() === "true") {
const fontName = 'VT323';
if (!document.getElementById('vt323-font')) {
const link = document.createElement('link');
link.id = 'vt323-font';
link.href = 'https://fonts.googleapis.com/css2?family=VT323&display=swap';
link.rel = 'stylesheet';
document.head.appendChild(link);
}
try {
// Ensure Google font has time to load, fallback otherwise
await document.fonts.load(`10px "${fontName}"`);
} catch(e) {}
const fontSize = Math.max(16, Math.floor(height * 0.08));
ctx.font = `${fontSize}px "${fontName}", 'Courier New', monospace`;
ctx.fillStyle = "#ffffff";
ctx.textAlign = "left";
ctx.textBaseline = "top";
// Add VHS text drop shadow for visibility
ctx.shadowColor = "rgba(0, 0, 0, 0.8)";
ctx.shadowOffsetX = 3;
ctx.shadowOffsetY = 3;
ctx.shadowBlur = 5;
// Status text
ctx.fillText("PLAY \u25BA", width * 0.05, height * 0.05); // \u25BA is a filled forward-pointing triangle
// Time code
ctx.fillText("0:00:00", width * 0.05, height * 0.88);
// Tape speed/mode
ctx.textAlign = "right";
ctx.fillText("SP", width * 0.95, height * 0.88);
// Current Date
const d = new Date();
const mm = ("0" + (d.getMonth() + 1)).slice(-2);
const dd = ("0" + d.getDate()).slice(-2);
const yy = String(d.getFullYear()).slice(-2);
ctx.fillText(`${mm}.${dd}.${yy}`, width * 0.95, height * 0.05);
// Reset shadow parameters completely
ctx.shadowColor = "transparent";
}
return canvas;
}
Apply Changes