You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, particleColorRGB = '255,255,255', particleOpacity = 0.7, minRadius = 10, maxRadius = 30, density = 0.8, blurAmount = 15) {
let imgWidth = originalImg.naturalWidth || originalImg.width;
let imgHeight = originalImg.naturalHeight || originalImg.height;
// Handle HTMLImageElement loading if it's not complete or has no dimensions
if (originalImg instanceof HTMLImageElement && (!originalImg.complete || originalImg.naturalWidth === 0 || originalImg.naturalHeight === 0)) {
// If the image is 'complete' but has no width/height, it might be broken or has no src.
if (originalImg.complete && (originalImg.naturalWidth === 0 || originalImg.naturalHeight === 0)) {
if (!originalImg.src) {
console.warn("Image has no source. Returning 1x1 canvas indicating error.");
} else {
console.warn("Image is marked complete but has zero dimensions (possibly broken). Returning 1x1 canvas.");
}
const errorCanvas = document.createElement('canvas');
errorCanvas.width = 1; errorCanvas.height = 1;
return errorCanvas;
}
// If not complete, or src is set but not yet loaded (naturalWidth is 0), try to load
if (!originalImg.src) {
console.warn("Image has no source and is not complete. Returning 1x1 canvas.");
const errorCanvas = document.createElement('canvas');
errorCanvas.width = 1; errorCanvas.height = 1;
return errorCanvas;
}
try {
await new Promise((resolve, reject) => {
// Assign new handlers
const loadHandler = () => {
imgWidth = originalImg.naturalWidth;
imgHeight = originalImg.naturalHeight;
cleanup();
resolve();
};
const errorHandler = () => {
cleanup();
reject(new Error("Image failed to load."));
};
const cleanup = () => {
originalImg.removeEventListener('load', loadHandler);
originalImg.removeEventListener('error', errorHandler);
};
originalImg.addEventListener('load', loadHandler);
originalImg.addEventListener('error', errorHandler);
// In case the image completes loading after the 'complete' check but before handlers are attached
if (originalImg.complete && originalImg.naturalWidth > 0) {
loadHandler();
}
});
} catch (e) {
console.error("Error during image loading:", e.message);
const errorCanvas = document.createElement('canvas');
errorCanvas.width = 1; errorCanvas.height = 1;
return errorCanvas;
}
}
// Final check on dimensions after any loading attempt
if (!imgWidth || !imgHeight || imgWidth === 0 || imgHeight === 0) {
console.warn("Image has invalid or zero dimensions after loading attempts. Returning 1x1 canvas.");
const errorCanvas = document.createElement('canvas');
errorCanvas.width = 1; errorCanvas.height = 1;
return errorCanvas;
}
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = imgWidth;
canvas.height = imgHeight;
// Draw the original image
ctx.drawImage(originalImg, 0, 0, imgWidth, imgHeight);
// Parameter validation and sanitization
// Coerce to string then to number for robustness against mixed types
const pOpacity = Math.max(0, Math.min(1, parseFloat(String(particleOpacity))));
let pMinRadius = Math.max(1, parseInt(String(minRadius), 10));
let pMaxRadius = Math.max(1, parseInt(String(maxRadius), 10));
if (pMinRadius > pMaxRadius) { // Ensure minRadius is not greater than maxRadius
[pMinRadius, pMaxRadius] = [pMaxRadius, pMinRadius];
}
const pDensity = Math.max(0.01, parseFloat(String(density))); // Density factor, avoid zero or negative
const pBlurAmount = Math.max(0, parseInt(String(blurAmount), 10));
// Parse particleColorRGB string (e.g., "255,255,255")
const rgbStrings = String(particleColorRGB).split(',');
const r_val = parseInt(rgbStrings[0] ? rgbStrings[0].trim() : '255', 10);
const g_val = parseInt(rgbStrings[1] ? rgbStrings[1].trim() : '255', 10);
const b_val = parseInt(rgbStrings[2] ? rgbStrings[2].trim() : '255', 10);
// Default to white if parsing fails for any component
const R = !isNaN(r_val) ? r_val : 255;
const G = !isNaN(g_val) ? g_val : 255;
const B = !isNaN(b_val) ? b_val : 255;
const finalParticleColor = `rgba(${R},${G},${B},${pOpacity})`;
// Shadow color can be same as particle or slightly adjusted (e.g., more transparent)
const shadowParticleColor = `rgba(${R},${G},${B},${pOpacity * 0.75})`;
// Setup particle drawing style
ctx.fillStyle = finalParticleColor;
if (pBlurAmount > 0) {
ctx.shadowColor = shadowParticleColor;
ctx.shadowBlur = pBlurAmount;
ctx.shadowOffsetX = 0; // Particle glow should be centered
ctx.shadowOffsetY = 0;
}
// Determine particle placement strategy
// avgRadius is used to scale the step size based on particle dimensions and density
const avgRadius = (pMinRadius + pMaxRadius) / 2;
// Step size for iterating grid. Inversely proportional to density.
// Higher density = smaller step = more particles.
const step = Math.max(1, avgRadius / pDensity);
// Loop through a grid and draw particles with jitter
// Extend loop bounds by pMaxRadius to ensure particles cover image edges
for (let y = -pMaxRadius; y < imgHeight + pMaxRadius; y += step) {
for (let x = -pMaxRadius; x < imgWidth + pMaxRadius; x += step) {
// Jitter position: random offset from grid point for a more natural look
// Jitter magnitude is proportional to step size.
const jitterX = (Math.random() - 0.5) * step;
const jitterY = (Math.random() - 0.5) * step;
// Base position is center of grid cell, then jitter is applied
const currentX = x + step / 2 + jitterX;
const currentY = y + step / 2 + jitterY;
// Randomize particle radius within the defined min/max range
const radius = pMinRadius + Math.random() * (pMaxRadius - pMinRadius);
// Optimization: skip drawing if particle is entirely off-canvas
if (currentX + radius < 0 || currentX - radius > imgWidth ||
currentY + radius < 0 || currentY - radius > imgHeight) {
continue;
}
ctx.beginPath();
ctx.arc(currentX, currentY, radius, 0, Math.PI * 2);
ctx.fill();
}
}
// Reset shadow properties to avoid affecting subsequent drawings if context is reused elsewhere
if (pBlurAmount > 0) {
ctx.shadowColor = 'transparent'; // Or 'rgba(0,0,0,0)'
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
}
return canvas;
}
Apply Changes