You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(
originalImg,
blurAmount = 2, // pixels, 0 for no blur
saturation = 1.4, // 1 for original, >1 for more saturation, <1 for less
contrast = 1.2, // 1 for original, >1 for more contrast, <1 for less
noiseAmount = 20, // 0-255 recommended, amount of grain. 0 for no noise.
tintColor = "rgba(20,20,80,0.1)", // CSS color string for tint overlay e.g. "rgba(R,G,B,A)"
vignetteStart = 0.3, // 0-1, normalized radius where vignette begins (0=center, 1=image edge)
vignetteEnd = 0.9, // 0-1, normalized radius where vignette reaches full strength (ideally >= vignetteStart)
vignetteColor = "rgba(0,0,0,0.6)", // CSS color string for vignette (alpha in RGBA controls strength)
lightLeakColor = "rgba(255,100,0,0.2)" // CSS color string for light leak (alpha in RGBA controls intensity)
) {
// Helper to parse rgba strings like "rgba(255,100,0,0.2)" or "rgb(255,100,0)"
function parseRgba(rgbaString = "") {
if (typeof rgbaString !== 'string') return { r: 0, g: 0, b: 0, a: 0, valid: false };
// Corrected regex to handle spaces before/after numbers and optional alpha
const match = rgbaString.toLowerCase().match(/rgba?\((\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})(?:\s*,\s*([\d.]+))?\)/);
if (!match) return { r: 0, g: 0, b: 0, a: 0, valid: false };
return {
r: parseInt(match[1]),
g: parseInt(match[2]),
b: parseInt(match[3]),
a: match[4] !== undefined ? parseFloat(match[4]) : 1,
valid: true
};
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Ensure originalImg is loaded, otherwise dimensions might be 0
// This function expects originalImg to be a fully loaded HTMLImageElement
canvas.width = originalImg.naturalWidth || originalImg.width;
canvas.height = originalImg.naturalHeight || originalImg.height;
if (canvas.width === 0 || canvas.height === 0) {
console.error("Image has zero dimensions. Ensure the image is loaded before processing.");
// Return an empty (but sized) canvas to avoid breaking downstream if possible
const errorCanvas = document.createElement('canvas');
errorCanvas.width = 100; // Default small size
errorCanvas.height = 100;
return errorCanvas;
}
// Create a temporary canvas for base image processing (blur, sat, con, noise)
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d', {willReadFrequently: true}); // for getImageData
tempCanvas.width = canvas.width;
tempCanvas.height = canvas.height;
// 1. Apply blur, saturation, contrast using canvas filter property
let filterString = "";
if (typeof blurAmount === 'number' && blurAmount > 0) {
filterString += `blur(${blurAmount}px) `;
}
if (typeof saturation === 'number' && saturation !== 1) {
filterString += `saturate(${saturation}) `;
}
if (typeof contrast === 'number' && contrast !== 1) {
filterString += `contrast(${contrast}) `;
}
tempCtx.filter = filterString.trim();
tempCtx.drawImage(originalImg, 0, 0, tempCanvas.width, tempCanvas.height);
tempCtx.filter = 'none'; // Reset filter before getImageData/putImageData
// 2. Apply noise
if (typeof noiseAmount === 'number' && noiseAmount > 0) {
const imageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
const data = imageData.data;
const clampedNoise = Math.max(0, Math.min(255, noiseAmount));
for (let i = 0; i < data.length; i += 4) {
// Apply monochromatic noise: random value added/subtracted from R, G, B
const noiseVal = (Math.random() - 0.5) * clampedNoise * 2;
data[i] = Math.max(0, Math.min(255, data[i] + noiseVal));
data[i + 1] = Math.max(0, Math.min(255, data[i + 1] + noiseVal));
data[i + 2] = Math.max(0, Math.min(255, data[i + 2] + noiseVal));
// Alpha (data[i+3]) remains unchanged
}
tempCtx.putImageData(imageData, 0, 0);
}
// 3. Draw processed base image from tempCanvas to the main output canvas
ctx.drawImage(tempCanvas, 0, 0);
// 4. Apply Tint overlay
const parsedTintColor = parseRgba(tintColor);
if (parsedTintColor.valid && parsedTintColor.a > 0) {
ctx.fillStyle = `rgba(${parsedTintColor.r},${parsedTintColor.g},${parsedTintColor.b},${parsedTintColor.a})`;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
// 5. Apply Vignette
const parsedVignetteColor = parseRgba(vignetteColor);
if (parsedVignetteColor.valid && parsedVignetteColor.a > 0 && vignetteEnd >= 0) { // vignetteEnd can be 0
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const maxRadius = Math.hypot(centerX, centerY); // Radius to canvas corner
// Clamp vignetteStart and vignetteEnd to [0, 1] range
const clampedVignetteStart = Math.max(0, Math.min(1, vignetteStart));
const clampedVignetteEnd = Math.max(0, Math.min(1, vignetteEnd));
let r_start_px = maxRadius * clampedVignetteStart;
let r_end_px = maxRadius * clampedVignetteEnd;
if (r_end_px <= r_start_px) {
// If end is not beyond start, make it a very thin feather at r_start_px,
// effectively a sharp edge if vignetteStart and vignetteEnd were equal.
r_end_px = r_start_px + 0.00001 * maxRadius;
}
// Only draw if start of vignette is within the image bounds
if (r_start_px < maxRadius) {
// Ensure r_end_px is at least slightly larger than r_start_px for gradient to render.
// It can extend beyond maxRadius, which is fine for gradients.
r_end_px = Math.max(r_end_px, r_start_px + 0.00001 * maxRadius);
const vignetteGradient = ctx.createRadialGradient(centerX, centerY, r_start_px, centerX, centerY, r_end_px);
// Transparent center of vignette
vignetteGradient.addColorStop(0, `rgba(${parsedVignetteColor.r},${parsedVignetteColor.g},${parsedVignetteColor.b},0)`);
// Full vignette color at the outer part
vignetteGradient.addColorStop(1, `rgba(${parsedVignetteColor.r},${parsedVignetteColor.g},${parsedVignetteColor.b},${parsedVignetteColor.a})`);
ctx.fillStyle = vignetteGradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
}
// 6. Apply Light Leak effect
const parsedLightLeakColor = parseRgba(lightLeakColor);
if (parsedLightLeakColor.valid && parsedLightLeakColor.a > 0) {
// Example: A large, soft radial flare from a fixed off-center position (top-left quadrant)
const leakX = canvas.width * 0.15;
const leakY = canvas.height * 0.15;
// Radius of the light leak effect, adjust for desired spread
const leakOuterRadius = Math.max(canvas.width, canvas.height) * 0.7;
const leakGradient = ctx.createRadialGradient(leakX, leakY, 0, leakX, leakY, leakOuterRadius);
// Light leak is brightest at its center, fading out
leakGradient.addColorStop(0, `rgba(${parsedLightLeakColor.r},${parsedLightLeakColor.g},${parsedLightLeakColor.b},${parsedLightLeakColor.a})`);
leakGradient.addColorStop(0.3, `rgba(${parsedLightLeakColor.r},${parsedLightLeakColor.g},${parsedLightLeakColor.b},${parsedLightLeakColor.a * 0.5})`); // Fades quite fast
leakGradient.addColorStop(1, `rgba(${parsedLightLeakColor.r},${parsedLightLeakColor.g},${parsedLightLeakColor.b},0)`); // Fully transparent at outer edge
ctx.globalCompositeOperation = 'lighter'; // 'screen' or 'overlay' also work well for light effects
ctx.fillStyle = leakGradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.globalCompositeOperation = 'source-over'; // Reset composite operation to default
}
return canvas;
}
Apply Changes