You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, aberrationAmount = 1.5, noiseAmount = 0.2, scanlineOpacity = 0.3, glitchAmount = 5, showTimestamp = 1, timestampText = '') {
/**
* Applies a VHS found footage analog effect to an image.
* @param {Image} originalImg - The source javascript Image object.
* @param {number} aberrationAmount - The strength of the chromatic aberration.
* @param {number} noiseAmount - The intensity of the monochrome noise (0 to 1).
* @param {number} scanlineOpacity - The opacity of the scanlines (0 to 1).
* @param {number} glitchAmount - The number of horizontal glitch blocks to generate.
* @param {number} showTimestamp - Set to 1 to show a timestamp, 0 to hide.
* @param {string} timestampText - Custom text for the timestamp. If empty, a random one is generated.
* @returns {Promise<HTMLCanvasElement>} A canvas element with the VHS effect applied.
*/
// 1. Setup Canvas
const canvas = document.createElement('canvas');
// Using { willReadFrequently: true } is a performance hint for the browser.
const ctx = canvas.getContext('2d', {
willReadFrequently: true
});
const w = originalImg.naturalWidth;
const h = originalImg.naturalHeight;
canvas.width = w;
canvas.height = h;
// 2. Draw the base image with an initial filter for an analog feel
ctx.filter = 'saturate(0.7) contrast(1.2) brightness(1.1) blur(0.5px)';
ctx.drawImage(originalImg, 0, 0, w, h);
ctx.filter = 'none';
// 3. Process Pixels for Core VHS Effects (Aberration, Noise, Scanlines)
const sourceData = ctx.getImageData(0, 0, w, h);
const sourcePixels = sourceData.data;
const outputData = ctx.createImageData(w, h);
const outputPixels = outputData.data;
// A random value to create a "wavy" horizontal distortion effect
const wavyDistortion = Math.random() * 10;
const aberration = Math.floor(aberrationAmount);
for (let y = 0; y < h; y++) {
// Calculate a horizontal shift for the entire line to simulate tracking issues
const lineShift = Math.floor((Math.sin(y * 0.04 + wavyDistortion) + Math.sin(y * 0.07 + wavyDistortion)) * 2);
for (let x = 0; x < w; x++) {
const outputIndex = (y * w + x) * 4;
const sourceX = Math.max(0, Math.min(w - 1, x + lineShift));
// --- Chromatic Aberration ---
// Get source pixel indices for R, G, B channels with horizontal offsets
const rIndex = (y * w + Math.max(0, Math.min(w - 1, sourceX - aberration))) * 4;
const gIndex = (y * w + sourceX) * 4;
const bIndex = (y * w + Math.max(0, Math.min(w - 1, sourceX + aberration))) * 4;
// --- Noise ---
const monoNoise = (Math.random() - 0.5) * 255 * noiseAmount;
// Combine a pixel from the R, G, B shifted sources and add noise
// The Uint8ClampedArray will automatically clamp values between 0 and 255.
outputPixels[outputIndex] = sourcePixels[rIndex] + monoNoise;
outputPixels[outputIndex + 1] = sourcePixels[gIndex + 1] + monoNoise;
outputPixels[outputIndex + 2] = sourcePixels[bIndex + 2] + monoNoise;
outputPixels[outputIndex + 3] = sourcePixels[gIndex + 3]; // Alpha
// --- Scanlines ---
// Darken pixels on every 3rd line to create a scanline effect
if (y % 3 !== 0) {
const s = 1 - scanlineOpacity;
outputPixels[outputIndex] *= s;
outputPixels[outputIndex + 1] *= s;
outputPixels[outputIndex + 2] *= s;
}
}
}
// 4. Put the manipulated pixel data back onto the canvas
ctx.putImageData(outputData, 0, 0);
// 5. Add Glitch Blocks
// This draws shifted slices of the canvas over itself to create a blocky glitch effect.
for (let i = 0; i < glitchAmount; i++) {
const x = Math.random() * w * 0.8;
const y = Math.random() * h;
const h_ = Math.random() * 50 + 1;
const w_ = Math.random() * w * 0.2 + w * 0.05;
const offset = (Math.random() - 0.5) * 40;
ctx.drawImage(canvas, x, y, w_, h_, x + offset, y, w_, h_);
}
// 6. Add Vignette for a CRT screen look
const grd = ctx.createRadialGradient(w / 2, h / 2, Math.min(w, h) / 3, w / 2, h / 2, Math.max(w, h) / 1.5);
grd.addColorStop(0, 'rgba(0,0,0,0)');
grd.addColorStop(1, 'rgba(0,0,0,0.4)');
ctx.fillStyle = grd;
ctx.fillRect(0, 0, w, h);
// 7. Add Timestamp
if (showTimestamp > 0) {
const fontName = 'VT323';
// Using a direct woff2 link is more reliable and avoids parsing CSS.
const fontUrl = `url(https://fonts.gstatic.com/s/vt323/v17/pxiKyp0ihIEF2isQFJXGdg.woff2)`;
const vhsFont = new FontFace(fontName, fontUrl);
try {
// Check if font is already available to avoid re-loading
if (!document.fonts.check(`12px ${fontName}`)) {
await vhsFont.load();
document.fonts.add(vhsFont);
}
// Generate a random timestamp text if none is provided
let finalTimestampText = timestampText;
if (!finalTimestampText) {
const year = Math.floor(Math.random() * 20) + 1985; // 1985-2004
const month = String(Math.floor(Math.random() * 12) + 1).padStart(2, '0');
const day = String(Math.floor(Math.random() * 28) + 1).padStart(2, '0');
const hour = String(Math.floor(Math.random() * 24)).padStart(2, '0');
const minute = String(Math.floor(Math.random() * 60)).padStart(2, '0');
finalTimestampText = `PLAY ${month}-${day}-${year} ${hour}:${minute}`;
}
const fontSize = Math.max(20, Math.floor(w / 35));
ctx.font = `${fontSize}px "${fontName}", monospace`;
ctx.fillStyle = 'rgba(255, 230, 150, 0.75)';
ctx.textBaseline = 'bottom';
ctx.textAlign = 'right';
// Add a slight text shadow to mimic CRT glow
ctx.shadowColor = 'orange';
ctx.shadowBlur = 8;
const margin = Math.floor(w * 0.03);
ctx.fillText(finalTimestampText, w - margin, h - margin);
// Reset shadow for subsequent drawing operations
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
} catch (e) {
console.error('VHS font could not be loaded:', e);
// If the font fails to load, we simply skip drawing the text.
}
}
// 8. Return the final canvas
return canvas;
}
Apply Changes