You can edit the below JavaScript code to customize the image tool.
Apply Changes
/**
* Applies an old Soviet TV video filter with artifacts and synthwave effects to an image.
* This function simulates CRT screen barrel distortion, RGB channel separation (chromatic aberration),
* scanlines, noise, a vignette, and a colored glow on the highlights.
*
* @param {HTMLImageElement} originalImg The original javascript Image object.
* @param {number} [scanlineIntensity=0.3] Controls the darkness of scanlines. Range 0 to 1.
* @param {number} [vignetteIntensity=0.8] Controls the darkness of the corner vignette. Range 0 to 1.
* @param {number} [noiseAmount=0.08] The amount of random static noise. Range 0 to 1.
* @param {number} [distortionStrength=0.1] The strength of the CRT barrel distortion.
* @param {number} [rgbShiftAmount=5] The amount of horizontal RGB channel separation in pixels.
* @param {number} [glowIntensity=0.5] The brightness intensity of the synthwave glow.
* @param {string} [glowColor='#00FFFF'] The color of the synthwave glow in hex format (e.g., '#FF00FF').
* @returns {Promise<HTMLCanvasElement>} A promise that resolves to a new canvas element with the filter applied.
*/
async function processImage(originalImg, scanlineIntensity = 0.3, vignetteIntensity = 0.8, noiseAmount = 0.08, distortionStrength = 0.1, rgbShiftAmount = 5, glowIntensity = 0.5, glowColor = '#00FFFF') {
// Helper to parse hex color string into an {r, g, b} object
const parseHexColor = (hex) => {
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; // Return null if parse fails
};
const glowRgb = parseHexColor(glowColor);
// Setup main canvas
const canvas = document.createElement('canvas');
// Using { willReadFrequently: true } is a performance hint for browsers
const ctx = canvas.getContext('2d', {
willReadFrequently: true
});
const w = originalImg.width;
const h = originalImg.height;
canvas.width = w;
canvas.height = h;
// Draw original image to a source canvas to securely get its pixel data
const sourceCanvas = document.createElement('canvas');
sourceCanvas.width = w;
sourceCanvas.height = h;
const sourceCtx = sourceCanvas.getContext('2d', {
willReadFrequently: true
});
sourceCtx.drawImage(originalImg, 0, 0, w, h);
const sourceData = sourceCtx.getImageData(0, 0, w, h);
const sourcePixels = sourceData.data;
// Prepare destination image data
const destData = ctx.createImageData(w, h);
const destPixels = destData.data;
// Pre-calculate constants for the loop
const cx = w / 2;
const cy = h / 2;
const maxRadius = Math.sqrt(cx * cx + cy * cy);
const vignettePower = 2.5; // Creates a more realistic falloff for the vignette
// A helper function for nearest-neighbor pixel sampling with boundary clamping
const getPixel = (pixels, x, y) => {
x = Math.round(x);
y = Math.round(y);
x = Math.max(0, Math.min(w - 1, x));
y = Math.max(0, Math.min(h - 1, y));
const i = (y * w + x) * 4;
return [pixels[i], pixels[i + 1], pixels[i + 2], pixels[i + 3]];
};
// Main loop to process every pixel of the destination image
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const destIndex = (y * w + x) * 4;
// --- 1. Barrel Distortion: Calculate source coordinates ---
const dx = x - cx;
const dy = y - cy;
const r = Math.sqrt(dx * dx + dy * dy);
// Calculate a bend factor based on distance from center
const bend = distortionStrength * Math.pow(r / maxRadius, 2);
// Apply the bend to find where to sample from the source image
const sx = cx + dx * (1 - bend);
const sy = cy + dy * (1 - bend);
// --- 2. RGB Shift: Sample R, G, B channels from different locations ---
const rPixel = getPixel(sourcePixels, sx + rgbShiftAmount, sy);
const gPixel = getPixel(sourcePixels, sx, sy);
const bPixel = getPixel(sourcePixels, sx - rgbShiftAmount, sy);
let rVal = rPixel[0];
let gVal = gPixel[1];
let bVal = bPixel[2];
let aVal = gPixel[3];
// --- 3. Scanline Effect ---
if (y % 3 === 0) {
const factor = 1 - scanlineIntensity;
rVal *= factor;
gVal *= factor;
bVal *= factor;
}
// --- 4. Noise Effect ---
const noise = (Math.random() - 0.5) * 255 * noiseAmount;
rVal += noise;
gVal += noise;
bVal += noise;
// --- 5. Vignette Effect ---
const vignette = Math.pow(Math.max(0, 1.0 - (r / maxRadius) * vignetteIntensity), vignettePower);
rVal *= vignette;
gVal *= vignette;
bVal *= vignette;
// --- Finalize: Clamp values and write to destination pixel ---
destPixels[destIndex] = Math.max(0, Math.min(255, rVal));
destPixels[destIndex + 1] = Math.max(0, Math.min(255, gVal));
destPixels[destIndex + 2] = Math.max(0, Math.min(255, bVal));
destPixels[destIndex + 3] = aVal;
}
}
ctx.putImageData(destData, 0, 0);
// --- 6. Synthwave Glow Effect (applied as a post-processing layer) ---
if (glowIntensity > 0 && glowRgb) {
// Create a highlight mask from the processed image
const glowCanvas = document.createElement('canvas');
glowCanvas.width = w;
glowCanvas.height = h;
const glowCtx = glowCanvas.getContext('2d', {
willReadFrequently: true
});
glowCtx.putImageData(destData, 0, 0);
const glowImageData = glowCtx.getImageData(0, 0, w, h);
const glowPixels = glowImageData.data;
for (let i = 0; i < glowPixels.length; i += 4) {
// Use luminance to find bright areas
const brightness = 0.2126 * glowPixels[i] + 0.7152 * glowPixels[i + 1] + 0.0722 * glowPixels[i + 2];
if (brightness > 180) { // Threshold for what is considered a "highlight"
glowPixels[i] = glowRgb.r;
glowPixels[i + 1] = glowRgb.g;
glowPixels[i + 2] = glowRgb.b;
} else {
glowPixels[i + 3] = 0; // Make non-highlight areas transparent
}
}
glowCtx.putImageData(glowImageData, 0, 0);
// Draw the blurred, glowing mask over the main image
ctx.save();
ctx.globalCompositeOperation = 'lighter';
const blurAmount = Math.max(5, Math.floor(w / 60));
ctx.filter = `blur(${blurAmount}px) brightness(${1.0 + glowIntensity})`;
ctx.drawImage(glowCanvas, 0, 0);
ctx.restore();
}
return canvas;
}
Apply Changes