You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(
originalImg,
rgbShiftStr = "4",
noiseLevelStr = "35",
scanlineIntensityStr = "0.2",
showOSDStr = "yes",
tint16mmStr = "yes"
) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Set canvas dimensions to match the original image
const w = originalImg.width;
const h = originalImg.height;
canvas.width = w;
canvas.height = h;
// Draw original image to canvas
ctx.drawImage(originalImg, 0, 0, w, h);
// Extract image data for pixel manipulation
const imgData = ctx.getImageData(0, 0, w, h);
const data = imgData.data;
const outData = new Uint8ClampedArray(data.length);
// Parse parameters
const rgbShift = parseInt(rgbShiftStr, 10);
const noiseLevel = parseFloat(noiseLevelStr);
const scanlineIntensity = parseFloat(scanlineIntensityStr);
const applyTint = (tint16mmStr.toLowerCase() === 'yes' || tint16mmStr === '1' || tint16mmStr.toLowerCase() === 'true');
const showOSD = (showOSDStr.toLowerCase() === 'yes' || showOSDStr === '1' || showOSDStr.toLowerCase() === 'true');
// Tracking band coordinates (simulating bottom VHS static)
const trackingY1 = Math.floor(h * 0.90);
const trackingY2 = Math.floor(h * 0.96);
// Loop through each pixel
for (let y = 0; y < h; y++) {
let scanOffset = 0;
// Simulating the distinct VHS tracking tear at the bottom
if (y > trackingY1 && y < trackingY2) {
scanOffset = Math.floor((Math.random() - 0.5) * (w * 0.03));
}
// Simulating slight top tape tearing
else if (y < h * 0.04) {
scanOffset = Math.floor(Math.sin((y / (h * 0.04)) * Math.PI) * (w * 0.02) * Math.random());
}
// Occasional random jitter for some scanlines
else if (Math.random() < 0.015) {
scanOffset = Math.floor((Math.random() - 0.5) * (w * 0.008));
}
const isScanline = (y % 3 === 0);
const slMult = 1 - scanlineIntensity;
const inTrackingBand = (y > trackingY1 && y < trackingY2);
for (let x = 0; x < w; x++) {
let srcX = x + scanOffset;
// Boundary clamping for the offset
if (srcX < 0) srcX = 0;
if (srcX >= w) srcX = w - 1;
// Calculate HIFI Stereo / Chromatic Aberration RGB shifts
let rX = srcX - rgbShift;
let bX = srcX + rgbShift;
let gX = srcX;
if (rX < 0) rX = 0; else if (rX >= w) rX = w - 1;
if (bX < 0) bX = 0; else if (bX >= w) bX = w - 1;
// Pixel indices
const iOut = (y * w + x) * 4;
const iR = (y * w + rX) * 4;
const iG = (y * w + gX) * 4;
const iB = (y * w + bX) * 4;
let r = data[iR];
let g = data[iG + 1];
let b = data[iB + 2];
// Add 16mm/VHS visual noise (Grain)
if (noiseLevel > 0) {
const noise = (Math.random() - 0.5) * noiseLevel;
r += noise;
g += noise;
b += noise;
}
// Apply heavy white static snow inside the tracking band
if (inTrackingBand && Math.random() < 0.35) {
const snow = 120 + Math.random() * 135;
r = snow;
g = snow;
b = snow;
}
// Apply Scanlines (darken every Nth horizontal line)
if (isScanline) {
r *= slMult;
g *= slMult;
b *= slMult;
}
// Simulate VHS color bleed by mixing original colors with Luma (desaturation + soften)
const luma = 0.299 * r + 0.587 * g + 0.114 * b;
r = r * 0.75 + luma * 0.25;
g = g * 0.75 + luma * 0.25;
b = b * 0.75 + luma * 0.25;
// Optional 16mm Film Tint (Adds warmth to the picture)
if (applyTint) {
r += 12; // Enhance red
g += 4; // Slight green boost
b -= 12; // Reduce blue
}
// Write processed values to outData
outData[iOut] = r;
outData[iOut + 1] = g;
outData[iOut + 2] = b;
outData[iOut + 3] = data[iOut + 3]; // Preserve original alpha
}
}
// Update canvas with distorted pixel data
imgData.data.set(outData);
ctx.putImageData(imgData, 0, 0);
// Apply Vintage Vignette overlay for 16mm lens feel
const gradient = ctx.createRadialGradient(
w / 2, h / 2, Math.min(w, h) * 0.4,
w / 2, h / 2, Math.max(w, h) * 0.8
);
gradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
gradient.addColorStop(1, 'rgba(0, 0, 0, 0.65)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, w, h);
// Render VCR On-Screen Display (OSD)
if (showOSD) {
ctx.shadowColor = 'rgba(0, 0, 0, 0.9)';
ctx.shadowBlur = Math.max(3, w * 0.005);
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.fillStyle = '#f8f8ff'; // Slightly off-white
// Base font size on image height
const fontSize = Math.max(16, Math.floor(h * 0.05));
ctx.font = `bold ${fontSize}px "Courier New", Courier, monospace`;
const paddingX = w * 0.05;
const paddingY = h * 0.08;
// Top Left - PLAY
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.fillText('PLAY \u25BA', paddingX, paddingY);
// Top Right - HIFI STEREO
ctx.textAlign = 'right';
ctx.fillText('HIFI STEREO', w - paddingX, paddingY);
// Bottom Left - Speed Mode
ctx.textAlign = 'left';
ctx.textBaseline = 'bottom';
ctx.fillText('SP', paddingX, h - paddingY);
// Generate Pseudo-Recording Date & Time
const d = new Date();
const dateStr = `${d.getFullYear().toString().substr(-2)} ${String(d.getMonth() + 1).padStart(2,'0')} ${String(d.getDate()).padStart(2,'0')}`;
let hours = d.getHours();
const ampm = hours >= 12 ? 'PM' : 'AM';
hours = hours % 12;
hours = hours ? hours : 12;
const timeStr = `${hours}:${String(d.getMinutes()).padStart(2, '0')}:${String(d.getSeconds()).padStart(2, '0')} ${ampm}`;
// Bottom Right - Timestamp
ctx.textAlign = 'right';
ctx.fillText(timeStr, w - paddingX, h - paddingY);
ctx.fillText(dateStr, w - paddingX, h - paddingY - (fontSize * 1.2));
// Reset shadows so they don't affect anything done later
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
}
return canvas;
}
Apply Changes