You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, intensity = 50, colorShiftAmount = 3, noiseAmount = 25, enableText = 1) {
const canvas = document.createElement('canvas');
const width = originalImg.width;
const height = originalImg.height;
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
// Draw original image
ctx.drawImage(originalImg, 0, 0);
// Grab pixel data for manipulation
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
const outData = new Uint8ClampedArray(data);
// Calculate dynamic effect scales based on image size
const shift = Math.max(1, Math.floor((width / 1000) * colorShiftAmount * (intensity / 50)));
// Contrast adjustment factor (adds that crunchy VHS look)
const contrast = 20 * (intensity / 50);
const contrastFactor = (259 * (contrast + 255)) / (255 * (259 - contrast));
// Pixel processing loop
for (let y = 0; y < height; y++) {
let jitter = 0;
// Define bottom tracking line error (VHS characteristic)
const isTrackingBand = (y > height * 0.90 && y < height * 0.95);
// Apply horizontal jitter
if (isTrackingBand) {
jitter = Math.floor((Math.random() - 0.5) * (width / 50) * (intensity / 50));
} else if (Math.random() < 0.03) {
// Random jitter for standard scanlines
jitter = Math.floor((Math.random() - 0.5) * (width / 200) * (intensity / 50));
}
for (let x = 0; x < width; x++) {
const i = (y * width + x) * 4;
// Chromatic aberration / Red-Blue shift
let rX = Math.max(0, Math.min(width - 1, x - shift + jitter));
let gX = Math.max(0, Math.min(width - 1, x + jitter));
let bX = Math.max(0, Math.min(width - 1, x + shift + jitter));
const iR = (y * width + rX) * 4;
const iG = (y * width + gX) * 4;
const iB = (y * width + bX) * 4;
let r = data[iR];
let g = data[iG + 1];
let b = data[iB + 2];
// Specific VHS Tracking static (speckles)
if (isTrackingBand && Math.random() < 0.15) {
r = g = b = 180 + Math.random() * 75;
}
// Apply contrast
r = contrastFactor * (r - 128) + 128;
g = contrastFactor * (g - 128) + 128;
b = contrastFactor * (b - 128) + 128;
// 16mm Color Grading (Slight warmth + green shadows typically seen in analog transfers)
r += 10;
g += 2;
b -= 5;
// Apply Film Grain / Noise
const noise = (Math.random() - 0.5) * (noiseAmount * 2) * (intensity / 50);
outData[i] = r + noise;
outData[i + 1] = g + noise;
outData[i + 2] = b + noise;
outData[i + 3] = 255; // Alpha
}
}
// Write processed pixels back
imageData.data.set(outData);
ctx.putImageData(imageData, 0, 0);
// Apply Overlays
// 1. Scanlines (VHS)
const scanlineHeight = Math.max(1, Math.floor(height / 400));
ctx.fillStyle = `rgba(0, 0, 0, 0.15)`;
for (let i = 0; i < height; i += scanlineHeight * 2) {
ctx.fillRect(0, i, width, scanlineHeight);
}
// 2. Vignette (16mm Film effect)
ctx.globalCompositeOperation = 'multiply';
const cx = width / 2;
const cy = height / 2;
const radius = Math.max(cx, cy);
const gradient = ctx.createRadialGradient(cx, cy, radius * 0.4, cx, cy, radius * 1.1);
gradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
gradient.addColorStop(1, 'rgba(0, 0, 0, 0.7)');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);
ctx.globalCompositeOperation = 'source-over';
// 3. Vertical Film Scratches (16mm Film effect)
for (let i = 0; i < 5; i++) {
let sX = Math.random() * width;
// White scratch
ctx.beginPath();
ctx.moveTo(sX, 0);
ctx.lineTo(sX + (Math.random() - 0.5) * 10, height);
ctx.strokeStyle = `rgba(255, 255, 255, ${Math.random() * 0.15 + 0.05})`;
ctx.lineWidth = Math.random() * 2 + 1;
ctx.stroke();
// Dark scratch
sX = Math.random() * width;
ctx.beginPath();
ctx.moveTo(sX, 0);
ctx.lineTo(sX + (Math.random() - 0.5) * 5, height);
ctx.strokeStyle = `rgba(0, 0, 0, ${Math.random() * 0.2 + 0.05})`;
ctx.lineWidth = Math.random() * 2 + 1;
ctx.stroke();
}
// 4. VHS Camcorder OSD / Date Text
if (Number(enableText) === 1) {
const fontSize = Math.max(20, Math.floor(height * 0.05));
ctx.font = `${fontSize}px "Courier New", Courier, monospace`;
ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
ctx.shadowColor = 'black';
ctx.shadowBlur = width * 0.005;
ctx.shadowOffsetX = width * 0.002;
ctx.shadowOffsetY = width * 0.002;
// "PLAY" icon
ctx.fillText("PLAY \u25BA", width * 0.05, height * 0.1);
// Timer/Date
ctx.fillText("OCT. 26 1985", width * 0.05, height * 0.85);
ctx.fillText("AM 01:20:00", width * 0.05, height * 0.85 + fontSize * 1.2);
// Reset shadow
ctx.shadowColor = 'transparent';
}
return canvas;
}
Apply Changes