You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, numRays = 1000, raySize = 1, rayColorStr = "255,255,255") {
// Sanitize numeric inputs
// numRays: number of speckles. Must be non-negative integer. Default to 1000 if invalid.
const DENSITY_NUM_RAYS = (typeof numRays === 'number' && isFinite(numRays) && numRays >= 0)
? Math.floor(numRays)
: 1000;
// raySize: size of each speckle in pixels. Must be positive integer (at least 1). Default to 1 if invalid.
const PIXEL_RAY_SIZE = (typeof raySize === 'number' && isFinite(raySize) && raySize >= 1)
? Math.floor(raySize)
: 1;
// Ensure the image is loaded and has valid dimensions
if (!originalImg.complete || originalImg.naturalWidth === 0 || originalImg.naturalHeight === 0) {
try {
await new Promise((resolve, reject) => {
const loadHandler = () => {
originalImg.removeEventListener('load', loadHandler);
originalImg.removeEventListener('error', errorHandler);
if (originalImg.naturalWidth === 0 || originalImg.naturalHeight === 0) {
reject(new Error("Image loaded but has zero dimensions."));
} else {
resolve();
}
};
const errorHandler = (errEvent) => {
originalImg.removeEventListener('load', loadHandler);
originalImg.removeEventListener('error', errorHandler);
// Try to provide a more specific error message if possible
const errorMsg = (typeof errEvent === 'string') ? errEvent :
(errEvent && errEvent.message) ? errEvent.message :
"Image failed to load for an unknown reason.";
reject(new Error(errorMsg));
};
originalImg.addEventListener('load', loadHandler);
originalImg.addEventListener('error', errorHandler);
// Handle cases where image might be 'complete' but errored or not yet processed by event listeners
if (originalImg.complete) {
if (originalImg.naturalWidth > 0 && originalImg.naturalHeight > 0) {
loadHandler(); // Already loaded successfully
} else {
// Already complete but dimensions are invalid, likely an error
errorHandler("Image is 'complete' but has invalid dimensions or failed to load.");
}
}
// Note: If originalImg.src is not set, this Promise may never resolve or reject.
// The caller is responsible for setting originalImg.src to initiate loading.
});
} catch (error) {
console.error("Image loading failed:", error.message);
const errorCanvas = document.createElement('canvas');
errorCanvas.width = Math.max(250, (error.message.length * 7) + 20); // Adjust width based on message
errorCanvas.height = 60;
const errorCtx = errorCanvas.getContext('2d');
if (errorCtx) {
errorCtx.fillStyle = "#f0f0f0"; // Light gray background
errorCtx.fillRect(0, 0, errorCanvas.width, errorCanvas.height);
errorCtx.fillStyle = "red";
errorCtx.font = "bold 14px Arial";
errorCtx.textAlign = "center";
errorCtx.fillText("Image Loading Error", errorCanvas.width / 2, 25);
errorCtx.fillStyle = "black";
errorCtx.font = "12px Arial";
// Truncate long messages for display
const displayMessage = error.message.length > 40 ? error.message.substring(0, 37) + "..." : error.message;
errorCtx.fillText(displayMessage, errorCanvas.width / 2, 45);
}
return errorCanvas;
}
}
const canvas = document.createElement('canvas');
canvas.width = originalImg.naturalWidth;
canvas.height = originalImg.naturalHeight;
// This should technically be caught by the loader, but as a final safeguard:
if (canvas.width === 0 || canvas.height === 0) {
console.warn("Image has zero dimensions after loading. Returning an empty canvas.");
return canvas; // Return empty 0x0 canvas
}
const ctx = canvas.getContext('2d');
if (!ctx) {
// Highly unlikely in a standard browser environment for '2d' context
console.error("Could not get 2D context from canvas. Returning an empty canvas.");
return canvas;
}
// Draw the original image onto the canvas
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
// If no rays are to be added, return the canvas with the original image drawn
if (DENSITY_NUM_RAYS === 0) {
return canvas;
}
let imageData;
try {
imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
} catch (e) {
console.error("Could not get image data from canvas:", e.message);
// This typically happens due to cross-origin security restrictions (tainted canvas)
ctx.clearRect(0,0,canvas.width, canvas.height); // Clear original image if we can't process
ctx.fillStyle = "rgba(100, 100, 100, 0.8)"; // Semi-transparent dark overlay
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "white";
ctx.textAlign = "center";
const baseFontSize = Math.min(canvas.width, canvas.height) / 18; // Responsive font size
ctx.font = `bold ${Math.max(12, baseFontSize)}px Arial`;
ctx.fillText("Effect Application Error", canvas.width / 2, canvas.height / 2 - baseFontSize * 0.6);
ctx.font = `${Math.max(10, baseFontSize * 0.7)}px Arial`;
const errDetail = e.message.toLowerCase().includes("taint") ? "Cross-origin image security issue." : "Cannot access pixel data.";
ctx.fillText(errDetail, canvas.width / 2, canvas.height / 2 + baseFontSize * 0.8);
return canvas;
}
const data = imageData.data; // Uint8ClampedArray: [R, G, B, A, R, G, B, A, ...]
// Parse rayColorStr (e.g., "255,0,128") into R, G, B components
let parsedR = 255, parsedG = 255, parsedB = 255; // Default to white
const colorString = String(rayColorStr || ""); // Ensure it's a string
const colorParts = colorString.split(',');
if (colorParts.length === 3) {
const tempR = parseInt(colorParts[0].trim(), 10);
const tempG = parseInt(colorParts[1].trim(), 10);
const tempB = parseInt(colorParts[2].trim(), 10);
if (!isNaN(tempR) && !isNaN(tempG) && !isNaN(tempB)) {
// Values will be clamped by Uint8ClampedArray, but good to keep within 0-255 logic
parsedR = Math.max(0, Math.min(255, tempR));
parsedG = Math.max(0, Math.min(255, tempG));
parsedB = Math.max(0, Math.min(255, tempB));
} else {
console.warn(`Invalid color components in rayColorStr: "${rayColorStr}". Using default white.`);
}
} else {
console.warn(`Invalid rayColorStr format: "${rayColorStr}". Expected "R,G,B". Using default white.`);
}
// Determine maximum top-left coordinates for a speckle to fit fully or partially
const maxX = Math.max(0, canvas.width - PIXEL_RAY_SIZE);
const maxY = Math.max(0, canvas.height - PIXEL_RAY_SIZE);
for (let i = 0; i < DENSITY_NUM_RAYS; i++) {
// Generate random top-left coordinates for the speckle
const x = (maxX > 0) ? Math.floor(Math.random() * (maxX + 1)) : 0; // Random int from 0 to maxX
const y = (maxY > 0) ? Math.floor(Math.random() * (maxY + 1)) : 0; // Random int from 0 to maxY
// Draw the speckle (a square of PIXEL_RAY_SIZE)
for (let dy = 0; dy < PIXEL_RAY_SIZE; dy++) {
const currentY = y + dy;
if (currentY >= canvas.height) break; // Stop if speckle row goes beyond canvas bottom
for (let dx = 0; dx < PIXEL_RAY_SIZE; dx++) {
const currentX = x + dx;
if (currentX >= canvas.width) break; // Stop if speckle col goes beyond canvas right
const index = (currentY * canvas.width + currentX) * 4;
data[index] = parsedR; // Red
data[index + 1] = parsedG; // Green
data[index + 2] = parsedB; // Blue
data[index + 3] = 255; // Alpha (fully opaque)
}
}
}
// Put the modified image data back onto the canvas
ctx.putImageData(imageData, 0, 0);
return canvas;
}
Apply Changes