You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, stops = 3) {
// Helper to create a standardized error canvas
const createErrorCanvas = (message, width = 100, height = 100) => {
const errCanvas = document.createElement('canvas');
// Ensure width and height are positive, otherwise canvas creation might fail or behave unpredictably
errCanvas.width = Math.max(1, width);
errCanvas.height = Math.max(1, height);
const errCtx = errCanvas.getContext('2d');
errCtx.fillStyle = '#EEEEEE'; // Light gray background
errCtx.fillRect(0, 0, errCanvas.width, errCanvas.height);
// Only attempt to draw text if canvas is reasonably sized
if (errCanvas.width >= 50 && errCanvas.height >= 20) {
errCtx.font = "bold 14px Arial";
errCtx.fillStyle = "red";
errCtx.textAlign = "center";
errCtx.textBaseline = "middle";
errCtx.fillText(message, errCanvas.width / 2, errCanvas.height / 2);
}
return errCanvas;
};
// Determine if the image source is loaded/ready
let isSourceReady = false;
if (originalImg instanceof HTMLImageElement) {
isSourceReady = originalImg.complete && originalImg.naturalWidth > 0 && originalImg.naturalHeight > 0;
} else if (originalImg instanceof HTMLVideoElement) {
isSourceReady = originalImg.readyState >= HTMLMediaElement.HAVE_METADATA; // HAVE_METADATA means dimensions are known
} else if (originalImg instanceof HTMLCanvasElement || originalImg instanceof ImageBitmap) {
isSourceReady = (originalImg.width || 0) > 0 && (originalImg.height || 0) > 0;
} else if (typeof OffscreenCanvas !== 'undefined' && originalImg instanceof OffscreenCanvas) {
isSourceReady = (originalImg.width || 0) > 0 && (originalImg.height || 0) > 0;
}
if (!isSourceReady) {
try {
if (originalImg instanceof HTMLImageElement && !originalImg.src) {
throw new Error("Image source (src) is not set.");
}
// Wait for the image/video to load
await new Promise((resolve, reject) => {
let eventTypeLoad = 'load';
let eventTypeError = 'error';
if (originalImg instanceof HTMLVideoElement) {
eventTypeLoad = 'loadedmetadata';
}
const loadHandler = () => {
originalImg.removeEventListener(eventTypeLoad, loadHandler);
originalImg.removeEventListener(eventTypeError, errorHandler);
resolve();
};
const errorHandler = (errEvent) => {
originalImg.removeEventListener(eventTypeLoad, loadHandler);
originalImg.removeEventListener(eventTypeError, errorHandler);
let errMessage = "Image failed to load.";
if (errEvent && errEvent.type) {
errMessage += ` (Event type: ${errEvent.type})`;
}
reject(new Error(errMessage));
};
originalImg.addEventListener(eventTypeLoad, loadHandler);
originalImg.addEventListener(eventTypeError, errorHandler);
// For HTMLImageElement, if src is set after addEventListener, it should work.
// If src was set long ago and failed, error handler should have fired or .complete might indicate status.
// This promise setup relies on events firing.
});
} catch (error) {
console.error("processImage Error (Loading):", error.message);
return createErrorCanvas("Load Error");
}
}
// Parse and validate 'stops' parameter
let numStops = parseFloat(String(stops));
if (isNaN(numStops)) {
console.warn(`Invalid 'stops' value: "${stops}". Using default value 3.`);
numStops = 3;
} else if (numStops < 0) {
console.warn(`Negative 'stops' value: "${stops}" is not typical for ND filters. Clamping to 0.`);
numStops = 0;
}
// Create canvas and context
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', {
willReadFrequently: true // Optimization hint for frequent getImageData/putImageData
});
// Set canvas dimensions from the source
canvas.width = originalImg.naturalWidth || originalImg.videoWidth || originalImg.width;
canvas.height = originalImg.naturalHeight || originalImg.videoHeight || originalImg.height;
// Handle cases where dimensions are still zero (e.g., broken media)
if (canvas.width === 0 || canvas.height === 0) {
console.warn("Image source has zero dimensions even after load attempt.");
return createErrorCanvas("Zero Dimensions", 100, 100); // Default size for this error
}
// Draw the image onto the canvas
try {
ctx.drawImage(originalImg, 0, 0, canvas.width, canvas.height);
} catch (drawError) {
console.error("Error drawing image to canvas:", drawError);
return createErrorCanvas("Draw Error", canvas.width, canvas.height);
}
// If no stops (numStops is 0), no filtering is needed. Return canvas with original image.
if (numStops === 0) {
return canvas;
}
// Get image data for pixel manipulation
let imageData;
try {
imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
} catch (getImageDataError) {
console.error("Error getting image data (e.g., canvas tainted by cross-origin image):", getImageDataError);
let msg = "Pixel Access Error";
if (getImageDataError.name === 'SecurityError') {
msg = "Cross-origin Error";
}
return createErrorCanvas(msg, canvas.width, canvas.height);
}
const data = imageData.data;
// Calculate reduction factor: 1 stop halves light, 2 stops quarters it, etc.
// Factor = 1 / (2^stops) = 0.5^stops
const reductionFactor = Math.pow(0.5, numStops);
// Apply the ND filter effect by reducing RGB values
for (let i = 0; i < data.length; i += 4) {
data[i] = Math.round(data[i] * reductionFactor); // Red
data[i + 1] = Math.round(data[i + 1] * reductionFactor); // Green
data[i + 2] = Math.round(data[i + 2] * reductionFactor); // Blue
// Alpha channel (data[i + 3]) remains unchanged
}
// Put the modified image data back onto the canvas
ctx.putImageData(imageData, 0, 0);
return canvas;
}
Apply Changes