You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, distortion = 0.2, scanlineOpacity = 0.15, scanlineSpacing = 3, vignetteStrength = 0.8, staticAmount = 25, desaturation = 0.5, tintColor = '#33ff33', tintOpacity = 0.05) {
const {
width,
height
} = originalImg;
// Helper to parse hex color strings
const hexToRgb = (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;
};
// Clamp parameters to valid ranges
distortion = Math.max(0, Math.min(1, distortion));
scanlineOpacity = Math.max(0, Math.min(1, scanlineOpacity));
scanlineSpacing = Math.max(1, scanlineSpacing);
vignetteStrength = Math.max(0, Math.min(1, vignetteStrength));
staticAmount = Math.max(0, Math.min(255, staticAmount));
desaturation = Math.max(0, Math.min(1, desaturation));
tintOpacity = Math.max(0, Math.min(1, tintOpacity));
const tint = hexToRgb(tintColor);
const destCanvas = document.createElement('canvas');
destCanvas.width = width;
destCanvas.height = height;
const destCtx = destCanvas.getContext('2d');
// --- 1. Apply barrel distortion ---
if (distortion > 0) {
const sourceCanvas = document.createElement('canvas');
sourceCanvas.width = width;
sourceCanvas.height = height;
const sourceCtx = sourceCanvas.getContext('2d', { willReadFrequently: true });
sourceCtx.drawImage(originalImg, 0, 0);
const sourceData = sourceCtx.getImageData(0, 0, width, height).data;
const destImageData = destCtx.createImageData(width, height);
const destData = destImageData.data;
const centerX = width / 2;
const centerY = height / 2;
const k = -distortion; // Negative k for barrel distortion
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
// Normalize coordinates to [-0.5, 0.5]
const nX = (x - centerX) / width;
const nY = (y - centerY) / height;
const r2 = nX * nX + nY * nY;
// Apply inverse distortion formula
const factor = 1 + k * r2;
const sX = centerX + nX * width / factor;
const sY = centerY + nY * height / factor;
const destIndex = (y * width + x) * 4;
if (sX >= 0 && sX < width && sY >= 0 && sY < height) {
// Nearest neighbor sampling for a retro look
const sourceX = Math.floor(sX);
const sourceY = Math.floor(sY);
const sourceIndex = (sourceY * width + sourceX) * 4;
destData[destIndex] = sourceData[sourceIndex];
destData[destIndex + 1] = sourceData[sourceIndex + 1];
destData[destIndex + 2] = sourceData[sourceIndex + 2];
destData[destIndex + 3] = sourceData[sourceIndex + 3];
} else {
// Pixels outside the source are black
destData[destIndex] = 0;
destData[destIndex + 1] = 0;
destData[destIndex + 2] = 0;
destData[destIndex + 3] = 255;
}
}
}
destCtx.putImageData(destImageData, 0, 0);
} else {
// If no distortion, just draw the image
destCtx.drawImage(originalImg, 0, 0);
}
// --- 2. Apply pixel-level effects (Color, Static) ---
const imageData = destCtx.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 gray = 0.299 * r + 0.587 * g + 0.114 * b;
r = r * (1 - desaturation) + gray * desaturation;
g = g * (1 - desaturation) + gray * desaturation;
b = b * (1 - desaturation) + gray * desaturation;
}
// Tint
if (tint && tintOpacity > 0) {
r = r * (1 - tintOpacity) + tint.r * tintOpacity;
g = g * (1 - tintOpacity) + tint.g * tintOpacity;
b = b * (1 - tintOpacity) + tint.b * tintOpacity;
}
// Static/Noise
if (staticAmount > 0) {
const noise = (Math.random() - 0.5) * staticAmount;
r += noise;
g += noise;
b += noise;
}
// Clamp values to 0-255 range
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));
}
destCtx.putImageData(imageData, 0, 0);
// --- 3. Apply overlays (Scanlines, Vignette) ---
// Scan lines
if (scanlineOpacity > 0) {
destCtx.fillStyle = `rgba(0, 0, 0, ${scanlineOpacity})`;
for (let y = 0; y < height; y += scanlineSpacing) {
destCtx.fillRect(0, y, width, 1);
}
}
// Vignette
if (vignetteStrength > 0) {
const outerRadius = Math.sqrt(Math.pow(width / 2, 2) + Math.pow(height / 2, 2));
// A smaller inner radius makes the center clear area smaller
const innerRadius = outerRadius * 0.3;
const gradient = destCtx.createRadialGradient(
width / 2, height / 2, innerRadius,
width / 2, height / 2, outerRadius
);
gradient.addColorStop(0, 'rgba(0,0,0,0)');
gradient.addColorStop(1, `rgba(0,0,0,${vignetteStrength})`);
destCtx.fillStyle = gradient;
destCtx.fillRect(0, 0, width, height);
}
return destCanvas;
}
Apply Changes