You can edit the below JavaScript code to customize the image tool.
Apply Changes
/**
* Applies an "Old Soviet TV" video filter to an image, simulating effects like
* color degradation, scanlines, noise, vignetting, and CRT screen curvature.
*
* @param {Image} originalImg The original javascript Image object.
* @param {number} desaturation=0.5 The amount of color desaturation (0 to 1). Default is 0.5.
* @param {string} colorTint='#80a080' The hex color code for the color tint. Default is a greenish '#80a080'.
* @param {number} tintIntensity=0.3 The intensity of the color tint (0 to 1). Default is 0.3.
* @param {number} scanlineIntensity=0.3 The opacity of the scanlines (0 to 1). Default is 0.3.
* @param {number} noiseIntensity=0.2 The intensity of the random noise (0 to 1). Default is 0.2.
* @param {number} barrelDistortion=0.4 The amount of barrel distortion to simulate a curved screen (0 to 1). Default is 0.4.
* @param {number} vignetteIntensity=0.7 The darkness of the vignette at the corners (0 to 1). Default is 0.7.
* @returns {HTMLCanvasElement} A canvas element with the filtered image.
*/
function processImage(originalImg, desaturation = 0.5, colorTint = '#80a080', tintIntensity = 0.3, scanlineIntensity = 0.3, noiseIntensity = 0.2, barrelDistortion = 0.4, vignetteIntensity = 0.7) {
const width = originalImg.naturalWidth;
const height = originalImg.naturalHeight;
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
// Use willReadFrequently for performance gains on repeated getImageData calls
const ctx = canvas.getContext('2d', {
willReadFrequently: true
});
// Helper to parse hex color string
const hexToRgb = (hex) => {
if (!hex || typeof hex !== 'string') return null;
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
};
const tintRgb = hexToRgb(colorTint);
// 1. Draw original image
ctx.drawImage(originalImg, 0, 0, width, height);
// 2. Apply pixel-level effects: Desaturation, Tint, and Noise
if (desaturation > 0 || tintIntensity > 0 || noiseIntensity > 0) {
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
let r = data[i];
let g = data[i + 1];
let b = data[i + 2];
// Desaturation
if (desaturation > 0) {
const avg = 0.299 * r + 0.587 * g + 0.114 * b; // Luminosity
r = r * (1 - desaturation) + avg * desaturation;
g = g * (1 - desaturation) + avg * desaturation;
b = b * (1 - desaturation) + avg * desaturation;
}
// Color Tint
if (tintIntensity > 0 && tintRgb) {
r = r * (1 - tintIntensity) + tintRgb.r * tintIntensity;
g = g * (1 - tintIntensity) + tintRgb.g * tintIntensity;
b = b * (1 - tintIntensity) + tintRgb.b * tintIntensity;
}
// Noise
if (noiseIntensity > 0) {
const noise = (Math.random() - 0.5) * 255 * noiseIntensity;
r += noise;
g += noise;
b += noise;
}
// Clamp values
data[i] = Math.max(0, Math.min(255, r));
data[i + 1] = Math.max(0, Math.min(255, g));
data[i + 2] = Math.max(0, Math.min(255, b));
}
ctx.putImageData(imageData, 0, 0);
}
// 3. Apply Barrel Distortion (CRT screen curve)
if (barrelDistortion > 0) {
const srcImageData = ctx.getImageData(0, 0, width, height);
const srcData = srcImageData.data;
const destImageData = ctx.createImageData(width, height);
const destData = destImageData.data;
const cx = width / 2;
const cy = height / 2;
const k = barrelDistortion / 2; // Distortion strength
const maxR2 = cx * cx + cy * cy; // Approx. max squared radius
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const dx = x - cx;
const dy = y - cy;
const r2 = dx * dx + dy * dy;
const factor = 1 + k * (r2 / maxR2);
const sx = cx + dx / factor;
const sy = cy + dy / factor;
const destIndex = (y * width + x) * 4;
if (sx >= 0 && sx < width && sy >= 0 && sy < height) {
// Use nearest-neighbor sampling
const srcX = Math.floor(sx);
const srcY = Math.floor(sy);
const srcIndex = (srcY * width + srcX) * 4;
destData[destIndex] = srcData[srcIndex];
destData[destIndex + 1] = srcData[srcIndex + 1];
destData[destIndex + 2] = srcData[srcIndex + 2];
destData[destIndex + 3] = srcData[srcIndex + 3];
} else {
// Fill with black for pixels outside the source
destData[destIndex] = 0;
destData[destIndex + 1] = 0;
destData[destIndex + 2] = 0;
destData[destIndex + 3] = 255;
}
}
}
ctx.putImageData(destImageData, 0, 0);
}
// 4. Apply Overlay Effects: Scanlines and Vignette
// Scanlines
if (scanlineIntensity > 0) {
ctx.fillStyle = `rgba(0, 0, 0, ${scanlineIntensity})`;
for (let y = 0; y < height; y += 3) {
ctx.fillRect(0, y, width, 1);
}
}
// Vignette
if (vignetteIntensity > 0) {
const outerRadius = Math.sqrt(width * width + height * height) / 2;
const gradient = ctx.createRadialGradient(width / 2, height / 2, 0, width / 2, height / 2, outerRadius);
gradient.addColorStop(0.3, 'rgba(0,0,0,0)');
gradient.addColorStop(1, `rgba(0,0,0,${vignetteIntensity})`);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, width, height);
}
return canvas;
}
Apply Changes