You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
effectToggles = "scanlines,noise,rgbShift,ui,grading",
noiseIntensity = 30,
rgbShiftAmount = 3,
timestampText = "OCT 24 1995"
) {
// 1. Setup the canvas
const canvas = document.createElement('canvas');
canvas.width = originalImg.width;
canvas.height = originalImg.height;
const ctx = canvas.getContext('2d');
// Draw the initial image
ctx.drawImage(originalImg, 0, 0);
const width = canvas.width;
const height = canvas.height;
// 2. Extract image data for pixel manipulation
const imgData = ctx.getImageData(0, 0, width, height);
const data = imgData.data;
// Create a copy to read un-mutated values for shifting effects
const originalData = new Uint8ClampedArray(data);
// Parsing toggles safely
const effects = effectToggles.toLowerCase();
const hasRgbShift = effects.includes('rgbshift');
const hasNoise = effects.includes('noise');
const hasGrading = effects.includes('grading');
const hasScanlines = effects.includes('scanlines');
const hasUI = effects.includes('ui');
const shiftX = Math.floor(rgbShiftAmount);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const i = (y * width + x) * 4;
let r = originalData[i];
let g = originalData[i + 1];
let b = originalData[i + 2];
// --- RGB Shift (Chromatic Aberration) ---
if (hasRgbShift) {
const shiftLeftX = Math.max(0, x - shiftX);
const shiftRightX = Math.min(width - 1, x + shiftX);
const shiftLeftIdx = (y * width + shiftLeftX) * 4;
const shiftRightIdx = (y * width + shiftRightX) * 4;
r = originalData[shiftLeftIdx]; // Red shifted from the left
b = originalData[shiftRightIdx + 2];// Blue shifted from the right
// Green stays the same
}
// --- Grading (Vintage, Faded, High-Contrast) ---
if (hasGrading) {
// Convert to luminance/grayscale for blending
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
// Desaturate slightly (approximate 40% desaturation)
const desaturation = 0.4;
r = r + (gray - r) * desaturation;
g = g + (gray - g) * desaturation;
b = b + (gray - b) * desaturation;
// Adjust Contrast (1.2) & Brightness (+15)
const contrast = 1.2;
const brightness = 15;
r = Math.min(255, Math.max(0, ((r / 255 - 0.5) * contrast + 0.5) * 255 + brightness));
g = Math.min(255, Math.max(0, ((g / 255 - 0.5) * contrast + 0.5) * 255 + brightness));
b = Math.min(255, Math.max(0, ((b / 255 - 0.5) * contrast + 0.5) * 255 + brightness));
// Mild Vignette Calculation
const dx = x - width / 2;
const dy = y - height / 2;
const distance = Math.sqrt(dx * dx + dy * dy);
const maxDistance = Math.sqrt((width / 2) ** 2 + (height / 2) ** 2);
const vignetteEffect = Math.max(0, distance / maxDistance);
// Deepen edges
r -= vignetteEffect * 40;
g -= vignetteEffect * 40;
b -= vignetteEffect * 40;
}
// --- Film/VHS Noise ---
if (hasNoise) {
const noiseAmount = (Math.random() - 0.5) * noiseIntensity * 2;
r = Math.min(255, Math.max(0, r + noiseAmount));
g = Math.min(255, Math.max(0, g + noiseAmount));
b = Math.min(255, Math.max(0, b + noiseAmount));
}
// Put processed pixel back
data[i] = r;
data[i + 1] = g;
data[i + 2] = b;
// alpha data[i+3] remains untouched
}
}
// Apply pixel data back to canvas
ctx.putImageData(imgData, 0, 0);
// --- CRT/VHS Scanlines ---
if (hasScanlines) {
ctx.fillStyle = 'rgba(0, 0, 0, 0.12)';
// Draw horizontal lines across the image
const scanlineWidth = Math.max(2, Math.floor(height * 0.005));
for (let y = 0; y < height; y += scanlineWidth * 2) {
ctx.fillRect(0, y, width, scanlineWidth);
}
}
// --- Home Video Style OSD UI ---
if (hasUI) {
// Load an appropriate retro font dynamically
const fontName = 'VT323';
if (!document.getElementById(`font-${fontName}`)) {
const link = document.createElement('link');
link.id = `font-${fontName}`;
link.rel = 'stylesheet';
link.href = `https://fonts.googleapis.com/css2?family=${fontName}&display=swap`;
document.head.appendChild(link);
}
try {
// Wait for the font to be ready to avoid default font flashes
if (document.fonts && document.fonts.ready) {
await document.fonts.load(`10pt "${fontName}"`);
await document.fonts.ready;
}
} catch (e) {
console.warn("Could not load Google Font. Falling back to system monospace.");
}
// Configure UI Font Size responsively
const fontSize = Math.max(20, Math.floor(height * 0.05));
ctx.font = `${fontSize}px "${fontName}", "Courier New", monospace`;
// Add a classic shadow for the OSD feel
ctx.shadowColor = 'black';
ctx.shadowBlur = Math.max(2, fontSize * 0.1);
ctx.shadowOffsetX = Math.max(1, fontSize * 0.05);
ctx.shadowOffsetY = Math.max(1, fontSize * 0.05);
const padding = fontSize;
// 1. Draw "REC" Text
ctx.fillStyle = '#ffffff';
ctx.textBaseline = 'top';
ctx.fillText('REC', padding, padding);
// 2. Draw blinking Red Dot (Simulated state)
// Calculating text width securely
const recMetrics = ctx.measureText('REC ');
ctx.fillStyle = '#ff1111';
ctx.fillText('●', padding + recMetrics.width, padding);
// 3. Draw Battery Icon (Top Right)
ctx.fillStyle = '#ffffff';
ctx.strokeStyle = '#ffffff';
const bW = fontSize * 2.5;
const bH = fontSize * 1.0;
const bX = width - bW - padding * 1.5;
const bY = padding;
ctx.lineWidth = Math.max(2, Math.floor(fontSize * 0.08));
// Battery Body
ctx.strokeRect(bX, bY, bW, bH);
// Battery Terminal (right stub)
ctx.fillRect(bX + bW + 2, bY + (bH * 0.25), bW * 0.1, bH * 0.5);
// Battery level bars (2 out of 3 blocks to look partially drained)
const gap = bW * 0.1;
const barW = (bW - (gap * 4)) / 3;
ctx.fillRect(bX + gap, bY + gap, barW, bH - gap * 2);
ctx.fillRect(bX + gap * 2 + barW, bY + gap, barW, bH - gap * 2);
// 4. Draw Timestamp (Bottom Left)
ctx.fillStyle = '#ffffff';
ctx.textBaseline = 'bottom';
// Add AM indicator line and then the customizable timestamp
ctx.fillText('AM 12:00', padding, height - padding - (fontSize * 1.2));
ctx.fillText(timestampText.toUpperCase(), padding, height - padding);
}
return canvas;
}
Apply Changes