You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, dateText = "MAR 3 1995", timeText = "12:00 AM", showPlay = "true", noiseLevel = 25) {
// Determine target dimensions limiting the pixel space to avoid browser freezing
const MAX_PIXELS = 1920 * 1080;
let scale = 1;
if (originalImg.width * originalImg.height > MAX_PIXELS) {
scale = Math.sqrt(MAX_PIXELS / (originalImg.width * originalImg.height));
}
const width = Math.floor(originalImg.width * scale);
const height = Math.floor(originalImg.height * scale);
// Load VCR/OSD style font (VT323) from Google Fonts
await new Promise((resolve) => {
const fontUrl = 'https://fonts.googleapis.com/css2?family=VT323&display=swap';
const link = document.createElement('link');
link.href = fontUrl;
link.rel = 'stylesheet';
link.onload = resolve;
link.onerror = resolve;
document.head.appendChild(link);
});
// Ensure the font is actually ready for Canvas usage
try {
await document.fonts.load(`20px "VT323"`);
} catch (e) {
// Continue silently; falls back to Courier New
}
// Canvas to hold the base image
const tempCanvas = document.createElement('canvas');
tempCanvas.width = width;
tempCanvas.height = height;
const tCtx = tempCanvas.getContext('2d');
tCtx.drawImage(originalImg, 0, 0, width, height);
// Minor vertical colour bleed imitation
tCtx.globalAlpha = 0.2;
tCtx.drawImage(tempCanvas, 0, 2);
// Main output canvas
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
// Background base
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, width, height);
ctx.globalAlpha = 1.0;
// Apply hardware-accelerated tracking distortions (wavy slices)
const sliceHeight = 2; // Process rows in pairs for performance
for (let y = 0; y < height; y += sliceHeight) {
// Continuous wave
let shiftX = Math.sin(y * 0.02) * 1.5;
// Apply heavier distortion on the bottom tracking band
if (y > height * 0.85 && y < height * 0.92) {
shiftX += (Math.random() - 0.5) * (width * 0.05); // heavy lateral jitter
} else if (Math.random() < 0.02) {
shiftX += (Math.random() - 0.5) * 4; // occasional micro jitter
}
ctx.drawImage(
tempCanvas,
0, y, width, sliceHeight,
shiftX, y, width, sliceHeight
);
}
// Apply Chromatic Aberration & Noise via ImageData
const imgData = ctx.getImageData(0, 0, width, height);
const data = imgData.data;
const copyData = new Uint8ClampedArray(data);
const shiftR = Math.max(1, Math.floor(width * 0.005));
const shiftB = Math.max(1, Math.floor(width * 0.003));
const noiseLvl = parseInt(Number(noiseLevel)) || 25;
for (let y = 0; y < height; y++) {
// Line-based noise for that distinct VHS static streakiness
let lineNoise = (Math.random() - 0.5) * noiseLvl * 0.6;
const isTrackingBand = y > height * 0.85 && y < Math.min(height * 0.92, height - 1);
for (let x = 0; x < width; x++) {
const i = (y * width + x) * 4;
// Chromatic shift Red channel left
const rx = x + shiftR;
if (rx < width) {
data[i] = copyData[(y * width + rx) * 4];
}
// Green remains unshifted (copyData[i + 1])
// Chromatic shift Blue channel right
const bx = x - shiftB;
if (bx >= 0) {
data[i + 2] = copyData[(y * width + bx) * 4 + 2];
}
// High static inside the tracking band
if (isTrackingBand && Math.random() < 0.25) {
const staticVal = Math.random() * 255;
data[i] = (data[i] + staticVal) * 0.5;
data[i + 1] = (data[i + 1] + staticVal) * 0.5;
data[i + 2] = (data[i + 2] + staticVal) * 0.5;
}
// General noise elsewhere
else {
const noise = lineNoise + (Math.random() - 0.5) * noiseLvl;
// Wash out the contrast slightly while adding noise
data[i] = Math.min(255, Math.max(0, data[i] * 0.95 + 10 + noise));
data[i + 1] = Math.min(255, Math.max(0, data[i + 1] * 0.95 + 10 + noise));
data[i + 2] = Math.min(255, Math.max(0, data[i + 2] * 0.95 + 10 + noise));
}
}
}
ctx.putImageData(imgData, 0, 0);
// Overlay scanlines
ctx.fillStyle = 'rgba(0, 0, 0, 0.15)';
for (let y = 0; y < height; y += 3) {
ctx.fillRect(0, y, width, 1);
}
// Render VCR/Camcorder OSD Overlays
const fontSize = Math.max(20, Math.floor(height * 0.07));
ctx.font = `normal ${fontSize}px "VT323", "Courier New", Courier, monospace`;
ctx.fillStyle = '#ffffff';
ctx.textBaseline = 'top';
// Helper to draw text with that glowing edge look typical of bright CRT osds
function drawGlowingText(text, x, y) {
ctx.shadowColor = 'rgba(0,0,0,1)';
ctx.shadowBlur = 4;
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.fillText(text, x, y);
ctx.shadowColor = 'rgba(255, 255, 255, 0.6)';
ctx.shadowBlur = 4;
ctx.shadowOffsetX = -1;
ctx.shadowOffsetY = -1;
ctx.fillText(text, x, y);
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
ctx.fillText(text, x, y);
}
const margin = Math.floor(Math.min(width, height) * 0.05);
// PLAY ► and SP indicators
if (showPlay.toString().toLowerCase() === 'true' || showPlay.toString() === '1') {
ctx.textAlign = 'left';
drawGlowingText("PLAY ►", margin, margin);
ctx.textAlign = 'right';
drawGlowingText("SP", width - margin, margin);
}
// Date Overlay (The Meme)
ctx.textAlign = 'left';
ctx.textBaseline = 'bottom';
drawGlowingText(dateText, margin, height - margin);
// Time Overlay
ctx.textAlign = 'right';
drawGlowingText(timeText, width - margin, height - margin);
return canvas;
}
Apply Changes