You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(
originalImg,
flareX_percent_param = 25,
flareY_percent_param = 25,
flareRadius_percent_param = 15,
flareColor_param = "255,255,200", // Default: pale yellow
flareOpacity_param = 0.6,
addStreaks_param = "true",
addGhosts_param = "true"
) {
// --- Parameter parsing and validation ---
const P_flareX_percent = parseFloat(flareX_percent_param);
const P_flareY_percent = parseFloat(flareY_percent_param);
const P_flareRadius_percent = parseFloat(flareRadius_percent_param);
const P_flareOpacity = parseFloat(flareOpacity_param);
// Apply defaults if parsing failed (resulted in NaN)
const validatedFlareXPercent = isNaN(P_flareX_percent) ? 25 : P_flareX_percent;
const validatedFlareYPercent = isNaN(P_flareY_percent) ? 25 : P_flareY_percent;
const validatedFlareRadiusPercent = isNaN(P_flareRadius_percent) ? 15 : P_flareRadius_percent;
const validatedFlareOpacity = isNaN(P_flareOpacity) ? 0.6 : Math.max(0, Math.min(1, P_flareOpacity));
let validatedFlareColorRGB = (flareColor_param || "255,255,200").split(',').map(c => parseInt(c.trim(), 10));
if (validatedFlareColorRGB.length !== 3 || validatedFlareColorRGB.some(isNaN)) {
validatedFlareColorRGB = [255, 255, 200]; // Default color if parsing fails
}
const validatedAddStreaks = String(addStreaks_param).toLowerCase() === 'true';
const validatedAddGhosts = String(addGhosts_param).toLowerCase() === 'true';
// --- Canvas setup ---
const canvas = document.createElement('canvas');
canvas.width = originalImg.naturalWidth || originalImg.width;
canvas.height = originalImg.naturalHeight || originalImg.height;
if (canvas.width === 0 || canvas.height === 0) {
console.warn("Image has zero width or height. Returning original image.");
// Create a 0x0 canvas or return original image
// To be displayable, a new empty 0x0 canvas is still an element.
// However, instruction says "return a single canvas (preferred), or an Image ... "
// If it cannot be processed returning originalImg seems safer if caller expects an Image.
// Let's be consistent and return a 0x0 canvas.
const emptyCanvas = document.createElement('canvas');
emptyCanvas.width = 0;
emptyCanvas.height = 0;
return emptyCanvas;
}
const ctx = canvas.getContext('2d');
if (!ctx) {
console.error("Could not get 2D context. Returning original image.");
// As above, return an empty canvas.
const emptyCanvas = document.createElement('canvas');
emptyCanvas.width = canvas.width; // keep dimensions if available
emptyCanvas.height = canvas.height;
return emptyCanvas;
}
// Draw original image onto the canvas
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
// --- Calculate flare pixel values ---
const flareX = canvas.width * (validatedFlareXPercent / 100);
const flareY = canvas.height * (validatedFlareYPercent / 100);
const baseRadius = Math.min(canvas.width, canvas.height) * (validatedFlareRadiusPercent / 100);
// Set composite operation for additive light effect (makes colors brighter)
ctx.globalCompositeOperation = 'lighter';
// --- 1. Main Flare Core (Brightest Point) ---
const coreOpacity = validatedFlareOpacity * 0.9;
ctx.fillStyle = `rgba(${validatedFlareColorRGB[0]}, ${validatedFlareColorRGB[1]}, ${validatedFlareColorRGB[2]}, ${coreOpacity})`;
ctx.beginPath();
ctx.arc(flareX, flareY, baseRadius * 0.15, 0, 2 * Math.PI);
ctx.fill();
// --- 2. Main Glow (Radial Gradient) ---
const glowOpacityStart = validatedFlareOpacity * 0.6;
const glowOpacityMid = validatedFlareOpacity * 0.3;
const glowGradient = ctx.createRadialGradient(flareX, flareY, baseRadius * 0.1, flareX, flareY, baseRadius);
glowGradient.addColorStop(0, `rgba(${validatedFlareColorRGB[0]}, ${validatedFlareColorRGB[1]}, ${validatedFlareColorRGB[2]}, ${glowOpacityStart})`);
glowGradient.addColorStop(0.5, `rgba(${validatedFlareColorRGB[0]}, ${validatedFlareColorRGB[1]}, ${validatedFlareColorRGB[2]}, ${glowOpacityMid})`);
glowGradient.addColorStop(1, `rgba(${validatedFlareColorRGB[0]}, ${validatedFlareColorRGB[1]}, ${validatedFlareColorRGB[2]}, 0)`);
ctx.fillStyle = glowGradient;
ctx.beginPath();
ctx.arc(flareX, flareY, baseRadius, 0, 2 * Math.PI);
ctx.fill();
// --- 3. Streaks ---
if (validatedAddStreaks && baseRadius > 0) {
const numStreaks = 12;
const streakBaseOpacity = validatedFlareOpacity * 0.25;
for (let i = 0; i < numStreaks; i++) {
const angle = (i / numStreaks) * 2 * Math.PI + (Math.random() - 0.5) * 0.1; // Full circle with slight randomness
const streakLength = baseRadius * (1.5 + Math.random() * 2.5); // Vary length
const streakOpacity = streakBaseOpacity * (0.5 + Math.random() * 0.5); // Vary opacity
const streakWidth = Math.max(1, baseRadius * 0.015 * (0.7 + Math.random() * 0.6)); // Vary width, min 1px
const endX = flareX + Math.cos(angle) * streakLength;
const endY = flareY + Math.sin(angle) * streakLength;
const streakGrad = ctx.createLinearGradient(flareX, flareY, endX, endY);
streakGrad.addColorStop(0, `rgba(${validatedFlareColorRGB[0]}, ${validatedFlareColorRGB[1]}, ${validatedFlareColorRGB[2]}, ${streakOpacity})`);
streakGrad.addColorStop(0.3, `rgba(${validatedFlareColorRGB[0]}, ${validatedFlareColorRGB[1]}, ${validatedFlareColorRGB[2]}, ${streakOpacity * 0.7})`);
streakGrad.addColorStop(1, `rgba(${validatedFlareColorRGB[0]}, ${validatedFlareColorRGB[1]}, ${validatedFlareColorRGB[2]}, 0)`);
ctx.strokeStyle = streakGrad;
ctx.lineWidth = streakWidth;
ctx.beginPath();
ctx.moveTo(flareX, flareY);
ctx.lineTo(endX, endY);
ctx.stroke();
}
}
// --- 4. Ghost Flares ---
if (validatedAddGhosts && baseRadius > 0) {
const imgCenterX = canvas.width / 2;
const imgCenterY = canvas.height / 2;
const vecX = imgCenterX - flareX; // Vector from flare to image center
const vecY = imgCenterY - flareY;
// Predefined characteristics for a few ghost flares
const ghostData = [
{ k: -0.5, sizeFactor: 0.25, colorShift: [30, -10, -20], opacityFactor: 0.15, sides: 6 }, // Reddish tint
{ k: 0.3, sizeFactor: 0.15, colorShift: [-10, 30, -10], opacityFactor: 0.20, sides: 5 }, // Greenish tint
{ k: 0.7, sizeFactor: 0.20, colorShift: [-20, -10, 30], opacityFactor: 0.18, sides: 7 }, // Bluish tint
{ k: 1.2, sizeFactor: 0.10, colorShift: [10, 10, -20], opacityFactor: 0.12, sides: 6 }, // Yellowish tint
{ k: 1.6, sizeFactor: 0.18, colorShift: [25, 0, -5], opacityFactor: 0.10, sides: 5 } // Reddish tint again
];
ghostData.forEach(g => {
const ghostX = flareX + g.k * vecX;
const ghostY = flareY + g.k * vecY;
// Basic check to avoid drawing ghosts too far off-canvas (simple optimization)
// This helps prevent excessive drawing outside the visible area.
const safetyMargin = baseRadius * Math.max(g.sizeFactor, 0.5) * 2;
if (ghostX < -safetyMargin || ghostX > canvas.width + safetyMargin ||
ghostY < -safetyMargin || ghostY > canvas.height + safetyMargin) {
return;
}
const ghostRadius = baseRadius * g.sizeFactor * (0.8 + Math.random() * 0.4); // Randomize size slightly
if (ghostRadius < 1) return; // Skip rendering if ghost is too small to be visible
const r = Math.max(0, Math.min(255, validatedFlareColorRGB[0] + g.colorShift[0]));
const gr = Math.max(0, Math.min(255, validatedFlareColorRGB[1] + g.colorShift[1]));
const b = Math.max(0, Math.min(255, validatedFlareColorRGB[2] + g.colorShift[2]));
const ghostOpacity = validatedFlareOpacity * g.opacityFactor * (0.8 + Math.random() * 0.4); // Randomize opacity slightly
const ghostGradient = ctx.createRadialGradient(ghostX, ghostY, 0, ghostX, ghostY, ghostRadius);
ghostGradient.addColorStop(0, `rgba(${r},${gr},${b},${ghostOpacity})`);
ghostGradient.addColorStop(0.6, `rgba(${r},${gr},${b},${ghostOpacity * 0.5})`);
ghostGradient.addColorStop(1, `rgba(${r},${gr},${b},0)`);
ctx.fillStyle = ghostGradient;
// Draw polygon for ghost (e.g., hexagon, pentagon to simulate lens iris shape)
const sides = g.sides;
ctx.beginPath();
const angleOffset = (Math.PI / sides); // Offset for polygon orientation
for (let i = 0; i < sides; i++) {
const angle = (i / sides) * 2 * Math.PI + angleOffset;
const px = ghostX + ghostRadius * Math.cos(angle);
const py = ghostY + ghostRadius * Math.sin(angle);
if (i === 0) {
ctx.moveTo(px, py);
} else {
ctx.lineTo(px, py);
}
}
ctx.closePath();
ctx.fill();
});
}
// Reset composite operation to default
ctx.globalCompositeOperation = 'source-over';
return canvas;
}
Apply Changes