You can edit the below JavaScript code to customize the image tool.
async function processImage(originalImg, scanlineIntensity = 0.2, noiseAmount = 20, chromaticAberrationOffset = 2, distortionFrequency = 0.05, distortionAmplitude = 1, desaturation = 0.1) {
// Ensure parameters are numbers as they might come from string sources (e.g., HTML attributes)
scanlineIntensity = Number(scanlineIntensity);
noiseAmount = Number(noiseAmount);
chromaticAberrationOffset = Number(chromaticAberrationOffset);
distortionFrequency = Number(distortionFrequency);
distortionAmplitude = Number(distortionAmplitude);
desaturation = Number(desaturation);
const canvas = document.createElement('canvas');
// Get context early for error drawing if needed.
// willReadFrequently hint can optimize getImageData/putImageData on some browsers.
const ctx = canvas.getContext('2d', { willReadFrequently: true });
// Helper function to draw error messages on the canvas
function drawError(message, detail = "") {
const requestedWidth = originalImg.width || 0; // Attempt to use requested width for canvas size
const requestedHeight = originalImg.height || 0;
canvas.width = Math.max(200, requestedWidth);
canvas.height = Math.max(100, requestedHeight);
ctx.fillStyle = '#DDDDDD'; // Light gray background
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = 'red';
ctx.textAlign = 'center';
const V_CENTER = canvas.height / 2;
ctx.font = 'bold 14px sans-serif';
ctx.fillText(message, canvas.width / 2, V_CENTER - (detail ? 10 : 0) );
if (detail) {
ctx.font = '12px sans-serif';
// Limit detail length to prevent overflow
ctx.fillText(detail.substring(0,100), canvas.width / 2, V_CENTER + 10);
}
console.error(message + (detail ? " - " + detail : ""));
return canvas;
}
// Ensure image is loaded. originalImg should be an HTMLImageElement.
// It might not be loaded if src was just set, or if it's an empty Image object.
if (!originalImg.complete || originalImg.naturalWidth === 0) {
// Check if there's a source attribute that could be loaded.
// originalImg.src can be the full URL or "" if not set/invalid.
// originalImg.currentSrc is the actual URL of the loaded image.
const hasValidSrc = originalImg.src || (originalImg.currentSrc && originalImg.currentSrc !== window.location.href);
if (hasValidSrc) {
try {
await new Promise((resolve, reject) => {
// If already complete and has dimensions (e.g. SVG loaded but naturalWidth was 0 initially)
if (originalImg.complete && originalImg.naturalWidth > 0 && originalImg.naturalHeight > 0) {
resolve();
return;
}
const existingOnload = originalImg.onload;
originalImg.onload = function(...args) {
if (typeof existingOnload === 'function') {
existingOnload.apply(originalImg, args);
}
resolve();
};
const existingOnerror = originalImg.onerror;
originalImg.onerror = function(...args) {
if (typeof existingOnerror === 'function') {
existingOnerror.apply(originalImg, args);
}
let errMsg = "Image failed to load.";
// Attempt to get a more specific error message
if (args[0] && typeof args[0] === 'object' && args[0].message) {
errMsg = args[0].message; // ErrorEvent
} else if (typeof args[0] === 'string') {
errMsg = args[0]; // Simple string error
}
reject(new Error(errMsg));
};
// This handles cases where the image is already in a 'complete' but broken state (e.g. invalid src)
// and onload/onerror might not fire again for an already processed src.
if (originalImg.complete && (originalImg.naturalWidth === 0 || originalImg.naturalHeight === 0)) {
reject(new Error("Image is 'complete' but has zero dimensions, possibly broken or invalid."));
}
});
} catch (error) {
return drawError('Error loading image.', error.message);
}
} else {
return drawError('Image has no valid source attribute.');
}
}
const width = originalImg.naturalWidth;
const height = originalImg.naturalHeight;
// Final check for valid dimensions after loading attempts
if (width === 0 || height === 0) {
return drawError('Image loaded but has zero dimensions.');
}
canvas.width = width;
canvas.height = height;
try {
ctx.drawImage(originalImg, 0, 0, width, height);
} catch (e) {
// This error can occur if 'originalImg' is not a valid image source for 'drawImage'
// (e.g. a broken image or an unsupported format that slipped through checks)
return drawError('Error drawing image to canvas.', e.message);
}
let imageData;
try {
imageData = ctx.getImageData(0, 0, width, height);
} catch (e) {
// This usually happens due to tainted canvas (e.g., drawing a cross-origin image without CORS)
const detailMsg = e.message + ( (e.name === "SecurityError") ? ' (Ensure image is from the same origin or has CORS headers enabled for cross-origin use).' : '' );
return drawError('Cannot process image: Canvas operation restricted.', detailMsg);
}
const originalData = Uint8ClampedArray.from(imageData.data); // Keep a clean copy for sampling
const outputData = imageData.data; // This is a reference, modifying it modifies imageData
// Helper function to get a single color channel value from the original image data.
// It handles boundary conditions by clamping coordinates to the image dimensions.
function getPixelChannel(data, x, y, w, h, channel) {
// Round coordinates to nearest pixel, then clamp to be within image bounds
const clampedX = Math.max(0, Math.min(Math.round(x), w - 1));
const clampedY = Math.max(0, Math.min(Math.round(y), h - 1));
return data[(clampedY * w + clampedX) * 4 + channel]; // R=0, G=1, B=2, A=3
}
for (let y = 0; y < height; y++) {
// Calculate horizontal distortion (wobble) for the current scanline
// The sine wave creates a smooth, wave-like distortion.
const yDistortion = distortionAmplitude * Math.sin(y * distortionFrequency);
for (let x = 0; x < width; x++) {
const outputIdx = (y * width + x) * 4; // Index for the current pixel in outputData
// Determine the source X coordinate in the original image, applying the y-axis distortion
const currentSourceX = x - yDistortion;
// Chromatic Aberration: Sample R, G, B channels from slightly offset horizontal positions
let r = getPixelChannel(originalData, currentSourceX - chromaticAberrationOffset, y, width, height, 0); // Red
let g = getPixelChannel(originalData, currentSourceX, y, width, height, 1); // Green
let b = getPixelChannel(originalData, currentSourceX + chromaticAberrationOffset, y, width, height, 2); // Blue
// Alpha is assumed to be fully opaque for VHS effect. If transparency is needed:
// let a = getPixelChannel(originalData, currentSourceX, y, width, height, 3);
// Desaturation: Reduce color intensity
if (desaturation > 0) {
const gray = 0.299 * r + 0.587 * g + 0.114 * b; // Standard NTSC luma coefficients
const satFactor = Math.max(0, Math.min(1, desaturation)); // Clamp desaturation factor
r = r * (1 - satFactor) + gray * satFactor;
g = g * (1 - satFactor) + gray * satFactor;
b = b * (1 - satFactor) + gray * satFactor;
}
// Noise: Add random intensity variations to pixel color components
if (noiseAmount > 0) {
// Generates noise ranging from -noiseAmount/2 to +noiseAmount/2
const noiseVal = (Math.random() - 0.5) * noiseAmount;
r += noiseVal;
g += noiseVal;
b += noiseVal;
}
// Scanlines: Darken alternating lines to simulate CRT display
if (scanlineIntensity > 0 && y % 2 !== 0) { // Apply to odd rows (or even, personal preference)
const factor = Math.max(0, 1 - Math.min(1, scanlineIntensity)); // Clamp intensity effect
r *= factor;
g *= factor;
b *= factor;
}
// Write processed pixel data to the output buffer.
// Uint8ClampedArray automatically clamps values to the 0-255 range.
outputData[outputIdx] = r;
outputData[outputIdx + 1] = g;
outputData[outputIdx + 2] = b;
outputData[outputIdx + 3] = 255; // Alpha channel (fully opaque)
}
}
// Draw the modified pixel data back onto the canvas
ctx.putImageData(imageData, 0, 0);
return canvas;
}
Free Image Tool Creator
Can't find the image tool you're looking for? Create one based on your own needs now!
The Image VHS Photo Filter is a web-based tool designed to apply a vintage VHS effect to your images. It simulates the nostalgic look of old video tapes by incorporating various adjustments, such as scanline effects, chromatic aberration, and noise. Users can customize parameters like scanline intensity, noise levels, and color desaturation to achieve their desired aesthetic. This tool is ideal for creative projects, social media posts, or any situation where a retro aesthetic is desired, allowing users to transform digital images into visually appealing, nostalgic representations.