You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, leakColor1 = "rgba(255, 80, 30, 0.3)", leakColor2 = "rgba(255, 200, 50, 0.25)", leakIntensity = 0.8, grainAmount = 30, desaturationAmount = 0.25, vignetteStrength = 0.4, tintColor = "rgba(230, 200, 170, 0.1)") {
// Helper function to parse RGBA color strings
// Returns an object {r, g, b, a}
function parseRgbaString(rgbaStr) {
if (!rgbaStr || typeof rgbaStr !== 'string') return { r: 0, g: 0, b: 0, a: 0 };
// Regex to capture R, G, B, and optional Alpha from rgba() or rgb() string
const match = rgbaStr.toLowerCase().match(/rgba?\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*(\d*(?:\.\d+)?))?\)/);
if (!match) {
// Fallback for simple color names or hex (not fully supported, basic handling)
// For this tool, we expect RGBA strings mostly.
// A robust color parser would be needed for full CSS color support.
// console.warn("Invalid or unsupported color string format:", rgbaStr);
return { r: 0, g: 0, b: 0, a: 0 }; // Default to transparent black if parsing fails
}
return {
r: parseInt(match[1], 10),
g: parseInt(match[2], 10),
b: parseInt(match[3], 10),
a: match[4] !== undefined ? parseFloat(match[4]) : 1 // Default alpha to 1 if not specified (for rgb)
};
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const w = originalImg.naturalWidth || originalImg.width;
const h = originalImg.naturalHeight || originalImg.height;
canvas.width = w;
canvas.height = h;
// 1. Draw original image
ctx.drawImage(originalImg, 0, 0, w, h);
// 2. Apply Desaturation and Tint (if any)
// These are applied pixel by pixel for precise control
const parsedTint = parseRgbaString(tintColor);
if (desaturationAmount > 0 || (parsedTint && parsedTint.a > 0)) {
const imageData = ctx.getImageData(0, 0, w, h);
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];
// Apply desaturation
if (desaturationAmount > 0) {
const gray = 0.299 * r + 0.587 * g + 0.114 * b;
const sat = 1 - desaturationAmount;
r = r * sat + gray * desaturationAmount;
g = g * sat + gray * desaturationAmount;
b = b * sat + gray * desaturationAmount;
}
// Apply tint (alpha blending)
if (parsedTint && parsedTint.a > 0) {
const tintA = parsedTint.a;
r = r * (1 - tintA) + parsedTint.r * tintA;
g = g * (1 - tintA) + parsedTint.g * tintA;
b = b * (1 - tintA) + parsedTint.b * tintA;
}
data[i] = Math.max(0, Math.min(255, Math.round(r)));
data[i+1] = Math.max(0, Math.min(255, Math.round(g)));
data[i+2] = Math.max(0, Math.min(255, Math.round(b)));
}
ctx.putImageData(imageData, 0, 0);
}
// 3. Add Light Leaks
if (leakIntensity > 0 && (leakColor1 || leakColor2)) {
ctx.globalCompositeOperation = 'lighter'; // Blending mode for light effects
const numLeaks = Math.floor(Math.random() * 3) + 2; // Generate 2 to 4 leaks
for (let i = 0; i < numLeaks; i++) {
const useColor1 = Math.random() < 0.6;
const colorStr = useColor1 ? leakColor1 : (leakColor2 || leakColor1); // Fallback to leakColor1 if leakColor2 is null/empty
const parsedLeakColor = parseRgbaString(colorStr);
if (parsedLeakColor.a === 0) continue; // Skip if base color is transparent
const r = parsedLeakColor.r;
const g = parsedLeakColor.g;
const b = parsedLeakColor.b;
let a = parsedLeakColor.a * leakIntensity; // Modulate alpha by overall intensity
if (a <= 0) continue; // Skip if effectively transparent after intensity modulation
const currentLeakColorWithIntensity = `rgba(${r},${g},${b},${a})`;
const transparentLeakColor = `rgba(${r},${g},${b},0)`;
let cx, cy, rOuter;
const edge = Math.floor(Math.random() * 4); // 0:top, 1:right, 2:bottom, 3:left
const extentFactor = Math.random() * 0.6 + 0.5; // How far it extends (50%-110% of dimension)
switch(edge) {
case 0: // Top
cx = Math.random() * w;
cy = (Math.random() * 0.2 - 0.1) * h; // Centered around top edge
rOuter = (h * extentFactor) * (0.6 + Math.random() * 0.8); // Varying radius
break;
case 1: // Right
cx = w * (1 - (Math.random() * 0.2 - 0.1)); // Centered around right edge
cy = Math.random() * h;
rOuter = (w * extentFactor) * (0.6 + Math.random() * 0.8);
break;
case 2: // Bottom
cx = Math.random() * w;
cy = h * (1 - (Math.random() * 0.2 - 0.1)); // Centered around bottom edge
rOuter = (h * extentFactor) * (0.6 + Math.random() * 0.8);
break;
case 3: // Left
default:
cx = (Math.random() * 0.2 - 0.1) * w; // Centered around left edge
cy = Math.random() * h;
rOuter = (w * extentFactor) * (0.6 + Math.random() * 0.8);
break;
}
const rInnerRatio = Math.random() * 0.2 + 0.05; // Inner part of gradient (5%-25% of outer)
const gradient = ctx.createRadialGradient(cx, cy, Math.max(0, rOuter * rInnerRatio), cx, cy, Math.max(1, rOuter));
gradient.addColorStop(0, currentLeakColorWithIntensity);
const midStop = Math.random() * 0.3 + 0.2; // Color holds for 20-50% of the gradient
gradient.addColorStop(Math.min(1, midStop), currentLeakColorWithIntensity);
gradient.addColorStop(1, transparentLeakColor);
ctx.fillStyle = gradient;
ctx.fillRect(0,0,w,h); // Apply gradient over the whole canvas
}
ctx.globalCompositeOperation = 'source-over'; // Reset composite operation
}
// 4. Apply Film Grain
if (grainAmount > 0) {
const imageData = ctx.getImageData(0, 0, w, h);
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const noise = (Math.random() - 0.5) * grainAmount;
// Apply monochrome noise to R, G, B channels
data[i] = Math.max(0, Math.min(255, Math.round(data[i] + noise)));
data[i+1] = Math.max(0, Math.min(255, Math.round(data[i+1] + noise)));
data[i+2] = Math.max(0, Math.min(255, Math.round(data[i+2] + noise)));
// Alpha channel (data[i+3]) is untouched
}
ctx.putImageData(imageData, 0, 0);
}
// 5. Apply Vignette
if (vignetteStrength > 0) {
const centerX = w / 2;
const centerY = h / 2;
const maxDim = Math.max(w,h);
// Outer radius should typically cover the corners of the image
const outerRadius = Math.sqrt(centerX*centerX + centerY*centerY);
// r0: radius where the vignette effect starts (center is clear until this radius)
// As vignetteStrength increases, r0 decreases, making the vignette encroach more.
// The factor 0.7 controls how aggressively the vignette spreads with strength.
// (1 - vignetteStrength * 0.7) means:
// if strength=0, r0 = outerRadius (no vignette)
// if strength=1, r0 = 0.3 * outerRadius (strong vignette)
const r0 = outerRadius * (1 - Math.min(1, vignetteStrength) * 0.7);
const gradient = ctx.createRadialGradient(
centerX, centerY, Math.max(0, r0), // Inner circle radius
centerX, centerY, outerRadius // Outer circle radius
);
gradient.addColorStop(0, 'rgba(0,0,0,0)'); // Center of gradient is transparent
// The darkness of the vignette edge is also controlled by vignetteStrength
gradient.addColorStop(1, `rgba(0,0,0,${Math.min(1, vignetteStrength)})`);
ctx.fillStyle = gradient;
ctx.globalCompositeOperation = 'source-over'; // Apply on top
ctx.fillRect(0, 0, w, h);
}
return canvas;
}
Apply Changes