You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, leakColor1Str = "255,80,30,0.45", leakColor2Str = "255,200,50,0.35", leakPosition = "random", leakSizePercent = 70, numLeaksParam = 3, blendModeParam = "lighter") {
// 1. Create canvas and context
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Ensure originalImg dimensions are available
const imgWidth = originalImg.naturalWidth || originalImg.width;
const imgHeight = originalImg.naturalHeight || originalImg.height;
if (imgWidth === 0 || imgHeight === 0) {
// console.error("Image has zero dimensions. It might not be loaded properly.");
// Return a small, empty, or specific error-indicating canvas
canvas.width = 1;
canvas.height = 1;
return canvas;
}
canvas.width = imgWidth;
canvas.height = imgHeight;
// 2. Helper to parse color strings "r,g,b,a" to "rgba(r,g,b,a)"
function parseRgbaColor(colorStr, defaultColor) {
const parts = colorStr.split(',').map(s => s.trim());
if (parts.length !== 4) {
// console.warn(`Invalid color string format: "${colorStr}". Using default: ${defaultColor}`);
return defaultColor;
}
const r = parseInt(parts[0], 10);
const g = parseInt(parts[1], 10);
const b = parseInt(parts[2], 10);
const a = parseFloat(parts[3]);
if (isNaN(r) || isNaN(g) || isNaN(b) || isNaN(a)) {
// console.warn(`Invalid color values in: "${colorStr}". Using default: ${defaultColor}`);
return defaultColor;
}
const clampedR = Math.max(0, Math.min(255, r));
const clampedG = Math.max(0, Math.min(255, g));
const clampedB = Math.max(0, Math.min(255, b));
const clampedA = Math.max(0, Math.min(1, a));
return `rgba(${clampedR},${clampedG},${clampedB},${clampedA})`;
}
const parsedLeakColor1 = parseRgbaColor(leakColor1Str, "rgba(255,80,30,0.45)");
const parsedLeakColor2 = parseRgbaColor(leakColor2Str, "rgba(255,200,50,0.35)");
// 3. Draw original image
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
// 4. Prepare for applying light leaks
const numLeaks = Math.max(0, Math.floor(numLeaksParam));
if (numLeaks === 0) { // If no leaks, return canvas with original image
return canvas;
}
// Validate and set blend mode
const finalBlendMode = blendModeParam.toLowerCase();
// Common and effective blend modes for light effects
const allowedBlendModes = ["lighter", "screen", "overlay", "color-dodge", "add"];
// Note: "add" is not standard Canvas GCO, but "lighter" is often similar to "add" or "linear dodge (add)"
if (allowedBlendModes.includes(finalBlendMode)) {
ctx.globalCompositeOperation = finalBlendMode;
} else {
// console.warn(`Invalid or less common blendMode "${blendModeParam}". Defaulting to "lighter".`);
ctx.globalCompositeOperation = "lighter";
}
const w = canvas.width;
const h = canvas.height;
const diagonal = Math.sqrt(w * w + h * h);
const baseRadius = (Math.max(0, Math.min(150, leakSizePercent)) / 100) * diagonal; // Allow up to 150% for very large leaks
const validPositions = ["top", "bottom", "left", "right", "top-left", "top-right", "bottom-left", "bottom-right"];
let effectiveLeakPositionStrategy = leakPosition.toLowerCase();
if (effectiveLeakPositionStrategy !== "random" && !validPositions.includes(effectiveLeakPositionStrategy)) {
// console.warn(`Invalid leakPosition "${leakPosition}" provided. Defaulting to "random" strategy for leaks.`);
effectiveLeakPositionStrategy = "random";
}
for (let i = 0; i < numLeaks; i++) {
let currentActualLeakPos;
if (effectiveLeakPositionStrategy === "random") {
currentActualLeakPos = validPositions[Math.floor(Math.random() * validPositions.length)];
} else {
currentActualLeakPos = effectiveLeakPositionStrategy;
}
// Randomize size and shape of each leak
const outerRadius = baseRadius * (Math.random() * 0.7 + 0.3); // 30% to 100% of baseRadius
const innerRadius = outerRadius * (Math.random() * 0.4); // 0% to 40% of outerRadius (smaller innerRadius = harder edge)
let cx, cy;
// offScreenFactor determines how far off-canvas the center of the leak can be (as a fraction of its radius)
// A value of 0 means center is on edge, >0 means center is off-canvas.
const offScreenFactor = Math.random() * 0.4;
switch (currentActualLeakPos) {
case "top":
cx = w * Math.random();
cy = -outerRadius * offScreenFactor;
break;
case "bottom":
cx = w * Math.random();
cy = h + outerRadius * offScreenFactor;
break;
case "left":
cx = -outerRadius * offScreenFactor;
cy = h * Math.random();
break;
case "right":
cx = w + outerRadius * offScreenFactor;
cy = h * Math.random();
break;
case "top-left":
cx = -outerRadius * offScreenFactor;
cy = -outerRadius * offScreenFactor;
break;
case "top-right":
cx = w + outerRadius * offScreenFactor;
cy = -outerRadius * offScreenFactor;
break;
case "bottom-left":
cx = -outerRadius * offScreenFactor;
cy = h + outerRadius * offScreenFactor;
break;
case "bottom-right":
cx = w + outerRadius * offScreenFactor;
cy = h + outerRadius * offScreenFactor;
break;
}
const gradient = ctx.createRadialGradient(cx, cy, innerRadius, cx, cy, outerRadius);
// Alternate primary/secondary colors for leaks, or randomize
const c1 = (Math.random() < 0.5) ? parsedLeakColor1 : parsedLeakColor2;
const c2 = (c1 === parsedLeakColor1) ? parsedLeakColor2 : parsedLeakColor1;
gradient.addColorStop(0, c1);
// Mid color stop position, making leaks varied (e.g. soft or more defined)
gradient.addColorStop(Math.max(0.1, Math.min(0.7, Math.random() * 0.5 + 0.1)), c2);
gradient.addColorStop(1, "rgba(0,0,0,0)"); // Fade to fully transparent
ctx.fillStyle = gradient;
// Draw a circle filled with the gradient. The gradient is defined relative to this circle.
ctx.beginPath();
ctx.arc(cx, cy, outerRadius, 0, 2 * Math.PI);
ctx.fill();
}
// 5. Reset composite operation to default
ctx.globalCompositeOperation = 'source-over';
// 6. Return the canvas
return canvas;
}
Apply Changes