You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(
originalImg,
noiseIntensity = 0.15,
rgbShift = 4,
scanlineIntensity = 0.15,
blurAmount = 0.5,
showText = 1,
vhsText = "PLAY \u25BA"
) {
// Ensure input parameters are of correct numeric type
const noiseInt = Number(noiseIntensity);
const shiftAmount = Number(rgbShift);
const scanlineInt = Number(scanlineIntensity);
const blurAmt = Number(blurAmount);
const displayTxt = Number(showText);
const width = originalImg.width;
const height = originalImg.height;
// Create the primary canvas
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
// Step 1: Draw the image onto the canvas with a slight base blur/contrast boost for softness
ctx.filter = `blur(${blurAmt}px) contrast(1.1) saturate(1.1)`;
ctx.drawImage(originalImg, 0, 0);
ctx.filter = 'none';
// Step 2: Grab pixel data for heavy manipulation (VHS & 16mm organic artifacts)
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
const outData = new Uint8ClampedArray(data.length);
const maxDist = Math.sqrt(0.5); // Base reference for distance at corners (approx x: 0.5^2 + y: 0.5^2)
const bleed = 3; // Simulates chroma blur/color bleed horizontally common in VHS
// Iterate over all pixels
for (let y = 0; y < height; y++) {
// Tape Tracking Wiggle Distance (Wavy alignment lines + analog jitter)
const waveOffset = Math.floor(Math.sin(y * 0.02) * shiftAmount * 1.5) + (Math.random() > 0.95 ? 1 : 0);
for (let x = 0; x < width; x++) {
const i = (y * width + x) * 4;
const localX = x + waveOffset;
// -- Red Channel Process (Chroma Blur/Bleed) --
// Averages neighboring red pixels to create horizontal smeared tailing
let rSum = 0;
for (let k = 0; k < bleed; k++) {
const rx = Math.max(0, Math.min(width - 1, localX - shiftAmount - k));
rSum += data[(y * width + rx) * 4];
}
let r = rSum / bleed;
// -- Green Channel Process (Central Tracked Channel) --
const gx = Math.max(0, Math.min(width - 1, localX));
let g = data[(y * width + gx) * 4 + 1];
// -- Blue Channel Process (Shifted Right) --
const bx = Math.max(0, Math.min(width - 1, localX + shiftAmount));
let b = data[(y * width + bx) * 4 + 2];
// Add High-Fidelity 16mm Film Grain / VHS Static
const noise = (Math.random() - 0.5) * 255 * noiseInt;
r += noise;
g += noise;
b += noise;
// Inject Analog Scanlines (Interlacing artifacts on every 3rd pixel line)
if (y % 3 === 0) {
r -= r * scanlineInt;
g -= g * scanlineInt;
b -= b * scanlineInt;
}
// Calculate Vignette (Darkening edges)
const dx = (x / width) - 0.5;
const dy = (y / height) - 0.5;
const dist = Math.sqrt(dx * dx + dy * dy);
const vignetteStrength = 0.5; // Moderately dark edges
const vignette = 1 - Math.min(1, dist / maxDist) * vignetteStrength;
r *= vignette;
g *= vignette;
b *= vignette;
// -- 16mm Analog Color Grading & Exposure Map --
// Shift tones to be warmer (higher red/green, less blue) and lift absolute blacks
r = r * 0.96 + 18;
g = g * 0.92 + 12;
b = b * 0.85 + 5;
// Apply a slight S-Curve to midtones to mimic film emulation (Crush)
r = (r - 128) * 1.15 + 128;
g = (g - 128) * 1.15 + 128;
b = (b - 128) * 1.15 + 128;
outData[i] = r;
outData[i + 1] = g;
outData[i + 2] = b;
outData[i + 3] = data[i + 3]; // Preserve Original Alpha
}
}
// Step 3: Put processed image data back into the canvas
const outImgData = new ImageData(outData, width, height);
ctx.putImageData(outImgData, 0, 0);
// Step 4: Form 16mm physical Scratches and Dust specs
const numScratches = Math.floor(width / 150);
for (let i = 0; i < numScratches; i++) {
const scX = Math.random() * width;
const scW = Math.random() * 2 + 1;
// Light scratch
ctx.fillStyle = `rgba(255, 255, 255, ${Math.random() * 0.06 + 0.02})`;
ctx.fillRect(scX, 0, scW, height);
// Dark dust streak
const scDarkX = Math.random() * width;
ctx.fillStyle = `rgba(0, 0, 0, ${Math.random() * 0.1 + 0.03})`;
ctx.fillRect(scDarkX, 0, scW, height);
}
// Step 5: VHS Tape tracking error bands (Random thin horizontal noise lines)
const numCreases = Math.floor(Math.random() * 3) + 1;
for (let i = 0; i < numCreases; i++) {
const cy = Math.random() * height;
const ch = Math.random() * (height * 0.02) + 2;
// White static block
ctx.fillStyle = 'rgba(255, 255, 255, 0.08)';
ctx.fillRect(0, cy, width, ch);
// Sub-channel RGB band inside crease
ctx.fillStyle = Math.random() > 0.5 ? 'rgba(255, 0, 0, 0.06)' : 'rgba(0, 255, 0, 0.06)';
ctx.fillRect(0, cy + ch / 3, width, ch / 3);
}
// Step 6: Render VHS OSD overlay metrics
if (displayTxt === 1 && String(vhsText).trim() !== "") {
const fontSize = Math.max(22, Math.floor(height * 0.045));
// Use a blocky native monospaced font
ctx.font = `bold ${fontSize}px "Courier New", Courier, monospace`;
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
// Glowing tube shadow setup for text
ctx.shadowColor = 'rgba(0, 0, 0, 0.9)';
ctx.shadowBlur = Math.max(3, fontSize * 0.15);
ctx.shadowOffsetX = Math.max(1, fontSize * 0.05);
ctx.shadowOffsetY = Math.max(1, fontSize * 0.05);
ctx.fillStyle = '#ffffff';
const padding = fontSize;
ctx.fillText(String(vhsText), padding, padding);
// Add standard timing layout text at the bottom
const bottomText = "SP 0:00:00";
ctx.textBaseline = 'bottom';
ctx.fillText(bottomText, padding, height - padding);
// Add static date/time marker to right bottom (e.g. AM 12:00)
ctx.textAlign = 'right';
ctx.fillText("AM 12:00", width - padding, height - padding);
ctx.shadowColor = 'transparent'; // Cleanup
}
return canvas;
}
Apply Changes