You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(
originalImg,
stereoOffset = 4, // Spread of the red/blue channel split (chromatic aberration)
contrastLevel = 45, // High contrast 16mm look (0 - 100)
noiseAmount = 40, // Overall film grain intensity (0 - 100)
glitchAmount = 25, // VHS horizontal tracking shift intensity (0 - 100)
scanlineOpacity = 0.15 // Opacity of the VHS scanlines (0.0 - 1.0)
) {
// 1. Core Canvas Setup
const canvas = document.createElement('canvas');
const w = originalImg.width;
const h = originalImg.height;
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
// Draw base image slightly blurred to simulate degraded VHS/16mm focus
ctx.filter = 'blur(0.5px)';
ctx.drawImage(originalImg, 0, 0);
// 2. Glitch Shifts (VHS Tracking Glitches)
if (glitchAmount > 0) {
// Draw horizontal shifted slices from original image
const numGlitches = Math.floor(glitchAmount * 0.5);
for (let i = 0; i < numGlitches; i++) {
let sy = Math.floor(Math.random() * h);
let sh = Math.floor(Math.random() * (h * 0.05)) + 2;
let dx = (Math.random() - 0.5) * glitchAmount * (w / 200);
ctx.drawImage(originalImg, 0, sy, w, sh, dx, sy, w, sh);
}
}
ctx.filter = 'none';
// 3. Pixel Manipulation (Mono Grayscale, Contrast, Grain, Stereo Channel Shift)
const imgData = ctx.getImageData(0, 0, w, h);
const data = imgData.data;
const monoData = new Uint8ClampedArray(data.length);
// Calculate contrast factor
const factor = (259 * (contrastLevel + 255)) / (255 * (259 - contrastLevel));
// First pass: Generate Mono, Contrast, and Grain data
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i + 1];
let b = data[i + 2];
// Mono/Grayscale
let lum = 0.299 * r + 0.587 * g + 0.114 * b;
// Apply Contrast
lum = factor * (lum - 128) + 128;
// Apply 16mm Noise / Grain
let noise = (Math.random() - 0.5) * noiseAmount * 2;
lum += noise;
// Clamp values
lum = Math.min(255, Math.max(0, lum));
monoData[i] = lum;
monoData[i + 1] = lum;
monoData[i + 2] = lum;
monoData[i + 3] = data[i + 3];
}
// Second pass: Apply "HIFI Stereo" chromatic split
let offsetBytes = Math.floor(stereoOffset * Math.max(1, w / 800)) * 4;
for (let y = 0; y < h; y++) {
let rowStart = y * w * 4;
let rowEnd = (y + 1) * w * 4 - 4;
for (let x = 0; x < w; x++) {
let i = (y * w + x) * 4;
let rIdx = i - offsetBytes;
let bIdx = i + offsetBytes;
// Constrain red/blue shift inside the horizontal limits of the current row
let rVal = (rIdx >= rowStart) ? monoData[rIdx] : monoData[i];
let bVal = (bIdx <= rowEnd) ? monoData[bIdx] : monoData[i];
let gVal = monoData[i + 1]; // Green stays center
data[i] = rVal;
data[i + 1] = gVal;
data[i + 2] = bVal;
// alpha remains exactly the same
}
}
// Commit modified pixels
ctx.putImageData(imgData, 0, 0);
// 4. VHS Bottom Tracking Static
if (glitchAmount > 0) {
const trackH = Math.max(5, Math.floor(h * 0.04));
const trackY = h - trackH - Math.floor(Math.random() * (h * 0.02));
if(trackY > 0) {
const trackData = ctx.getImageData(0, trackY, w, trackH);
for (let i = 0; i < trackData.data.length; i += 4) {
if (Math.random() < 0.6) { // Snow effect
let gray = Math.random() * 255;
trackData.data[i] = gray;
trackData.data[i + 1] = gray;
trackData.data[i + 2] = gray;
}
}
ctx.putImageData(trackData, 0, trackY);
// Draw tracking distortion line
ctx.fillStyle = 'rgba(255,255,255,0.7)';
ctx.fillRect(0, trackY + trackH / 2, w, Math.max(1, Math.floor(h / 400)));
}
}
// 5. Classic Overlay Aesthetics (Text Play Timestamp & 16mm Scratches)
// Draw "PLAY" UI
ctx.fillStyle = 'rgba(230, 230, 230, 0.9)';
ctx.font = 'bold ' + Math.max(16, Math.floor(h * 0.04)) + 'px "Courier New", Courier, monospace';
ctx.shadowColor = 'black';
ctx.shadowBlur = 4;
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.fillText("PLAY \u25BA", w * 0.05, h * 0.05);
ctx.textAlign = 'right';
ctx.fillText("SP 0:00:00", w * 0.95, h * 0.05);
ctx.shadowBlur = 0; // Reset Shadow
// 16mm Dust and Scratches
const numScratches = Math.floor(Math.random() * 5) + 1;
ctx.strokeStyle = 'rgba(255, 255, 255, 0.25)';
for (let i = 0; i < numScratches; i++) {
ctx.beginPath();
let px = Math.random() * w;
ctx.moveTo(px, 0);
ctx.lineWidth = Math.random() * 1.5 + 0.5;
// Jagged vertical line down the frame
for (let py = 0; py <= h; py += Math.random() * 50 + 20) {
px += (Math.random() - 0.5) * 3;
ctx.lineTo(px, py);
}
ctx.stroke();
}
// 6. VHS Scanlines
if (scanlineOpacity > 0) {
ctx.fillStyle = `rgba(0, 0, 0, ${scanlineOpacity})`;
let lineThickness = Math.max(1, Math.floor(h / 600));
let lineSpacing = lineThickness * 3;
for (let y = 0; y < h; y += lineSpacing) {
ctx.fillRect(0, y, w, lineThickness);
}
}
// 7. Dark Edge Vignette
const grad = ctx.createRadialGradient(
w / 2, h / 2, Math.max(w, h) * 0.35,
w / 2, h / 2, Math.max(w, h) * 0.75
);
grad.addColorStop(0, 'rgba(0,0,0,0)');
grad.addColorStop(1, 'rgba(0,0,0,0.65)');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, w, h);
return canvas;
}
Apply Changes