You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(
originalImg,
grainAmount = 0.15, // 0 to 1 (0 = no grain, 0.15 = typical, 1 = very coarse)
desaturationAmount = 0.2, // 0 to 1 (0 = original saturation, 1 = grayscale)
sepiaAmount = 0.3, // 0 to 1 (0 = no sepia, 1 = full sepia tint)
contrastAmount = 1.1, // 0.5 to 2.0 (1 = original contrast, >1 more, <1 less)
vignetteAmount = 0.6, // 0 to 1 (0 = no vignette, 1 = strong vignette darkening edges)
lightLeakStrength = 0.25, // 0 to 1 (0 = no leaks, 1 = strong, more opaque leaks)
softnessAmount = 0.1 // 0 to 1 (0 = sharp, maps to ~0-1px blur for softness)
) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = originalImg.naturalWidth || originalImg.width;
canvas.height = originalImg.naturalHeight || originalImg.height;
// 1. Initial Draw (with optional softness/blur)
let initialDrawFilter = '';
// Clamp softnessAmount to a reasonable range for blur (e.g., 0-1 for 0-1px)
const softAmount = Math.max(0, Math.min(1, softnessAmount));
if (softAmount > 0) {
const blurRadius = softAmount * 1.0; // Max 1px blur for softnessAmount=1
initialDrawFilter += `blur(${blurRadius.toFixed(1)}px) `;
}
if (initialDrawFilter) {
ctx.filter = initialDrawFilter.trim();
}
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
ctx.filter = 'none'; // Reset filter
// Get image data for pixel manipulation
let imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
let data = imageData.data;
// 2. Per-pixel effects: Desaturation, Sepia, Grain
const applyPixelOps = grainAmount > 0 || desaturationAmount > 0 || sepiaAmount > 0;
if (applyPixelOps) {
const desat = Math.max(0, Math.min(1, desaturationAmount));
const sep = Math.max(0, Math.min(1, sepiaAmount));
const grain = Math.max(0, Math.min(1, grainAmount));
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 (desat > 0) {
const gray = 0.299 * r + 0.587 * g + 0.114 * b; // Luminance
r = r * (1 - desat) + gray * desat;
g = g * (1 - desat) + gray * desat;
b = b * (1 - desat) + gray * desat;
}
// --- Sepia ---
if (sep > 0) {
const current_r = r;
const current_g = g;
const current_b = b;
const sepiaR = current_r * 0.393 + current_g * 0.769 + current_b * 0.189;
const sepiaG = current_r * 0.349 + current_g * 0.686 + current_b * 0.168;
const sepiaB = current_r * 0.272 + current_g * 0.534 + current_b * 0.131;
r = current_r * (1 - sep) + sepiaR * sep;
g = current_g * (1 - sep) + sepiaG * sep;
b = current_b * (1 - sep) + sepiaB * sep;
}
// --- Grain ---
if (grain > 0) {
// Noise intensity: grain=0.1 results in noise range of approx +/- 12.75
const noise = (Math.random() - 0.5) * 255 * grain;
r += noise;
g += noise;
b += noise;
}
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. Contrast adjustment (globally, using CSS-like filter)
const contrast = Math.max(0, contrastAmount); // Ensure contrast is not negative
if (contrast !== 1.0) {
const tempCanvas = document.createElement('canvas');
tempCanvas.width = canvas.width;
tempCanvas.height = canvas.height;
const tempCtx = tempCanvas.getContext('2d');
tempCtx.drawImage(canvas, 0, 0); // Copy current main canvas to temp
ctx.clearRect(0, 0, canvas.width, canvas.height); // Clear main canvas
ctx.filter = `contrast(${contrast * 100}%)`;
ctx.drawImage(tempCanvas, 0, 0); // Draw temp canvas (with original content) back to main applying filter
ctx.filter = 'none'; // Reset filter
}
// 4. Vignette
const vigAmount = Math.max(0, Math.min(1, vignetteAmount));
if (vigAmount > 0) {
ctx.save();
ctx.globalCompositeOperation = 'multiply';
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const outerRadius = Math.sqrt(centerX * centerX + centerY * centerY);
const innerRadius = outerRadius * (1 - vigAmount);
const gradient = ctx.createRadialGradient(
centerX, centerY, innerRadius,
centerX, centerY, outerRadius
);
const vignetteColorDarkness = Math.min(1.0, vigAmount * 1.2);
gradient.addColorStop(0, `rgba(0,0,0,0)`);
gradient.addColorStop(1, `rgba(0,0,0,${vignetteColorDarkness})`);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.restore();
}
// 5. Light Leaks
const effectiveLeakStrength = Math.max(0, Math.min(1, lightLeakStrength));
if (effectiveLeakStrength > 0 && Math.random() < 0.65) { // Chance to apply leaks
ctx.save();
ctx.globalCompositeOperation = 'lighter';
const numLeaks = Math.floor(Math.random() * 2) + 1;
for (let i = 0; i < numLeaks; i++) {
const leakType = Math.random();
const rCol = 180 + Math.random() * 75;
const gCol = 50 + Math.random() * 100;
const bCol = Math.random() * 50;
const alpha = (0.15 + Math.random() * 0.3) * effectiveLeakStrength;
if (leakType < 0.6) { // Radial leak
const x = Math.random() * canvas.width;
const y = Math.random() * canvas.height;
const r1 = Math.random() * Math.min(canvas.width, canvas.height) * 0.05;
const r2 = r1 + (Math.random() * Math.min(canvas.width, canvas.height) * 0.3);
const grad = ctx.createRadialGradient(x, y, r1, x, y, r2);
grad.addColorStop(0, `rgba(${rCol}, ${gCol}, ${bCol}, ${alpha})`);
grad.addColorStop(1, `rgba(${rCol}, ${gCol}, ${bCol}, 0)`);
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(x, y, r2, 0, Math.PI * 2);
ctx.fill();
} else { // Linear leak
const x1 = Math.random() * canvas.width;
const y1 = Math.random() * canvas.height;
// Make x2, y2 somewhat distant and angled from x1, y1
const angle = Math.random() * Math.PI * 2;
const length = (Math.random() * 0.5 + 0.3) * Math.max(canvas.width, canvas.height); // 30-80% of max dimension
const x2 = x1 + Math.cos(angle) * length;
const y2 = y1 + Math.sin(angle) * length;
const grad = ctx.createLinearGradient(x1, y1, x2, y2);
grad.addColorStop(0, `rgba(${rCol}, ${gCol}, ${bCol}, ${alpha * 0.9})`);
grad.addColorStop(0.5, `rgba(${Math.max(0,rCol-20)}, ${Math.max(0,gCol-20)}, ${bCol}, ${alpha * 0.5})`);
grad.addColorStop(1, `rgba(${Math.max(0,rCol-40)}, ${Math.max(0,gCol-40)}, ${bCol}, 0)`);
ctx.strokeStyle = grad;
ctx.lineWidth = (Math.random() * 0.08 + 0.02) * Math.min(canvas.width, canvas.height) ;
ctx.beginPath();
// Using the calculated x1,y1,x2,y2 directly for line is fine
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
}
ctx.restore();
}
return canvas;
}
Apply Changes