You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, amplitudeStr = "10", frequencyStr = "0.1", phaseStr = "0", centerXRatioStr = "0.5", centerYRatioStr = "0.5") {
// Helper function to create an error canvas with a message
const createErrorCanvas = (width, height, message, baseImage = null) => {
const errCanvas = document.createElement('canvas');
// Ensure width and height are at least 1, default to 100 if not sensible
errCanvas.width = (width && width > 0) ? width : 100;
errCanvas.height = (height && height > 0) ? height : 100;
const errCtx = errCanvas.getContext('2d');
// Background: try to draw original image, else plain lightgray
if (baseImage && baseImage.width > 0 && baseImage.height > 0) {
try {
// Preserve aspect ratio while fitting image onto error canvas
let drawW = errCanvas.width;
let drawH = errCanvas.height;
const imgAspect = baseImage.width / baseImage.height;
const canvasAspect = errCanvas.width / errCanvas.height;
if (imgAspect > canvasAspect) { // Image wider than canvas aspect ratio
drawH = errCanvas.width / imgAspect;
} else { // Image taller or same aspect ratio
drawW = errCanvas.height * imgAspect;
}
// Center the drawn image
const drawX = (errCanvas.width - drawW) / 2;
const drawY = (errCanvas.height - drawH) / 2;
errCtx.drawImage(baseImage, drawX, drawY, drawW, drawH);
} catch (e) { // Fallback if drawing baseImage fails
errCtx.fillStyle = 'lightgray';
errCtx.fillRect(0, 0, errCanvas.width, errCanvas.height);
}
} else {
errCtx.fillStyle = 'lightgray';
errCtx.fillRect(0, 0, errCanvas.width, errCanvas.height);
}
// Text styling for the error message
const rectHeight = Math.max(40, errCanvas.height * 0.25); // Height of the text band
const textY = errCanvas.height / 2; // Y position for the text
errCtx.fillStyle = 'rgba(200, 0, 0, 0.75)'; // Semi-transparent red band
errCtx.fillRect(0, textY - rectHeight / 2, errCanvas.width, rectHeight);
errCtx.fillStyle = 'white';
errCtx.textAlign = 'center';
errCtx.textBaseline = 'middle';
// Dynamic font size, trying to fit message
const maxFontSize = Math.max(12, rectHeight * 0.4);
let fontSize = maxFontSize;
if (message && message.length > 0) {
fontSize = Math.min(maxFontSize, (errCanvas.width * 0.9) / (message.length * 0.6)); // Simple fit logic
}
errCtx.font = `bold ${fontSize}px Arial`;
errCtx.fillText(message, errCanvas.width / 2, textY);
return errCanvas;
};
// Parameter parsing
const amplitude = Number(amplitudeStr);
const frequency = Number(frequencyStr);
const phase = Number(phaseStr);
let centerXRatioNum = Number(centerXRatioStr);
let centerYRatioNum = Number(centerYRatioStr);
// Validate parameters
if (isNaN(amplitude) || isNaN(frequency) || isNaN(phase) || isNaN(centerXRatioNum) || isNaN(centerYRatioNum)) {
console.error("Invalid parameters for ripple filter. Parameters must be numbers.");
let w = (originalImg && (originalImg.naturalWidth || originalImg.width)) || 0;
let h = (originalImg && (originalImg.naturalHeight || originalImg.height)) || 0;
return createErrorCanvas(w, h, "Error: Invalid Parameters", originalImg);
}
// Image loading check (specific to HTMLImageElement)
if (originalImg instanceof HTMLImageElement && !originalImg.complete) {
try {
await new Promise((resolve, reject) => {
// Ensure onload/onerror are attached before checking complete status again
originalImg.onload = resolve;
originalImg.onerror = () => reject(new Error("Image failed to load."));
// If image is already complete by the time handlers are set up, resolve manually.
if (originalImg.complete) resolve();
});
} catch (error) {
console.error(error.message);
return createErrorCanvas(originalImg.width, originalImg.height, "Error: Image Load Failed");
}
}
// Determine image dimensions, using naturalWidth/Height for HTMLImageElement if available and valid
const imgWidth = ('naturalWidth' in originalImg && originalImg.naturalWidth > 0) ? originalImg.naturalWidth : originalImg.width;
const imgHeight = ('naturalHeight' in originalImg && originalImg.naturalHeight > 0) ? originalImg.naturalHeight : originalImg.height;
if (!imgWidth || imgWidth === 0 || !imgHeight || imgHeight === 0) {
console.error("Image has zero or invalid dimensions.");
return createErrorCanvas(100, 100, "Error: Invalid Image Dimensions");
}
// Clamp center ratios to the [0, 1] range to ensure center is within image
centerXRatioNum = Math.max(0, Math.min(1, centerXRatioNum));
centerYRatioNum = Math.max(0, Math.min(1, centerYRatioNum));
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = imgWidth;
canvas.height = imgHeight;
try {
ctx.drawImage(originalImg, 0, 0, imgWidth, imgHeight);
} catch (e) {
// This can happen if originalImg is, e.g., a detached element or invalid source
console.error("Error drawing original image to canvas:", e);
return createErrorCanvas(imgWidth, imgHeight, "Error: Draw Image Failed");
}
let originalImageData;
try {
originalImageData = ctx.getImageData(0, 0, imgWidth, imgHeight);
} catch (e) {
// This typically happens due to CORS policy for cross-origin images
console.error("Could not get ImageData (CORS issue or tainted canvas?):", e);
// Pass originalImg to be drawn behind the error message if possible
return createErrorCanvas(imgWidth, imgHeight, "Error: Canvas Access (CORS)", originalImg);
}
const originalData = originalImageData.data;
const outputImageData = ctx.createImageData(imgWidth, imgHeight); // Use createImageData from context
const outputData = outputImageData.data;
const actualCenterX = imgWidth * centerXRatioNum;
const actualCenterY = imgHeight * centerYRatioNum;
for (let y = 0; y < imgHeight; y++) {
for (let x = 0; x < imgWidth; x++) {
const dx = x - actualCenterX; // Distance from center X
const dy = y - actualCenterY; // Distance from center Y
const distance = Math.sqrt(dx * dx + dy * dy);
let srcX, srcY;
if (distance === 0) { // Avoid division by zero for the exact center pixel
srcX = x;
srcY = y;
} else {
// Calculate ripple effect: a sinusoidal wave based on distance from center
const waveValue = Math.sin(distance * frequency + phase);
// Displacement amount based on amplitude and wave
const displacement = amplitude * waveValue;
// Normalized direction vector from center to current pixel (x,y)
const normalizedDx = dx / distance;
const normalizedDy = dy / distance;
// Calculate source coordinates for this destination pixel (x,y)
// The pixel at (x,y) gets its color from (srcX, srcY)
// This displaces sampling radially.
srcX = x + normalizedDx * displacement;
srcY = y + normalizedDy * displacement;
}
// Use nearest neighbor interpolation by rounding source coordinates
const roundSrcX = Math.round(srcX);
const roundSrcY = Math.round(srcY);
// Clamp coordinates to be within the image boundaries (edge clamping)
const clampedSrcX = Math.max(0, Math.min(imgWidth - 1, roundSrcX));
const clampedSrcY = Math.max(0, Math.min(imgHeight - 1, roundSrcY));
// Calculate array indices for pixel data
const srcPixelIndex = (clampedSrcY * imgWidth + clampedSrcX) * 4;
const destPixelIndex = (y * imgWidth + x) * 4;
// Copy RGBA values from source to destination
outputData[destPixelIndex] = originalData[srcPixelIndex]; // R
outputData[destPixelIndex + 1] = originalData[srcPixelIndex + 1]; // G
outputData[destPixelIndex + 2] = originalData[srcPixelIndex + 2]; // B
outputData[destPixelIndex + 3] = originalData[srcPixelIndex + 3]; // A
}
}
// Put the modified pixel data back onto the canvas
ctx.putImageData(outputImageData, 0, 0);
return canvas;
}
Apply Changes