You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, strength = 2.0, swirl = 0.5) {
// Helper function for bilinear interpolation
// Fetches pixel data from (sampleX, sampleY) using bilinear interpolation.
// Handles boundary conditions by clamping to edge.
function getBilinearPixel(imgPixelData, imgWidth, imgHeight, sampleX, sampleY) {
const x0 = Math.floor(sampleX);
const y0 = Math.floor(sampleY);
const x1 = x0 + 1;
const y1 = y0 + 1;
// Fractional parts for interpolation
const dx = sampleX - x0;
const dy = sampleY - y0;
const invDx = 1 - dx;
const invDy = 1 - dy;
const resultRGBA = [0, 0, 0, 0]; // R, G, B, A
for (let channel = 0; channel < 4; channel++) { // Iterate over R, G, B, A channels
// Clamp coordinates to be within image bounds for each of the 4 surrounding pixels
const clamped_y0 = Math.max(0, Math.min(imgHeight - 1, y0));
const clamped_y1 = Math.max(0, Math.min(imgHeight - 1, y1));
const clamped_x0 = Math.max(0, Math.min(imgWidth - 1, x0));
const clamped_x1 = Math.max(0, Math.min(imgWidth - 1, x1));
// Get pixel values of the four surrounding points
const P00 = imgPixelData[(clamped_y0 * imgWidth + clamped_x0) * 4 + channel]; // Top-left
const P10 = imgPixelData[(clamped_y0 * imgWidth + clamped_x1) * 4 + channel]; // Top-right
const P01 = imgPixelData[(clamped_y1 * imgWidth + clamped_x0) * 4 + channel]; // Bottom-left
const P11 = imgPixelData[(clamped_y1 * imgWidth + clamped_x1) * 4 + channel]; // Bottom-right
// Bilinear interpolation formula
const topInterpolation = P00 * invDx + P10 * dx;
const bottomInterpolation = P01 * invDx + P11 * dx;
resultRGBA[channel] = topInterpolation * invDy + bottomInterpolation * dy;
}
return resultRGBA;
}
const width = originalImg.naturalWidth || originalImg.width;
const height = originalImg.naturalHeight || originalImg.height;
if (width === 0 || height === 0) {
// Return an empty canvas or handle error if image dimensions are invalid
const emptyCanvas = document.createElement('canvas');
emptyCanvas.width = 0;
emptyCanvas.height = 0;
console.warn("Image has zero width or height.");
return emptyCanvas;
}
// Create an input canvas to draw the original image and get its pixel data
const inputCanvas = document.createElement('canvas');
inputCanvas.width = width;
inputCanvas.height = height;
const inputCtx = inputCanvas.getContext('2d', { willReadFrequently: true }); // Hint for optimization
inputCtx.drawImage(originalImg, 0, 0, width, height);
let srcImageData;
try {
srcImageData = inputCtx.getImageData(0, 0, width, height);
} catch (e) {
// This can happen if the canvas is tainted (e.g., cross-origin image without CORS)
console.error("Error getting image data from input canvas:", e);
// As a fallback, return a canvas with the original image drawn (no filter)
const fallbackCanvas = document.createElement('canvas');
fallbackCanvas.width = width;
fallbackCanvas.height = height;
const fallbackCtx = fallbackCanvas.getContext('2d');
fallbackCtx.drawImage(originalImg, 0, 0, width, height);
return fallbackCanvas;
}
const srcData = srcImageData.data;
// Create an output canvas to draw the processed image
const outputCanvas = document.createElement('canvas');
outputCanvas.width = width;
outputCanvas.height = height;
const outputCtx = outputCanvas.getContext('2d');
const destImageData = outputCtx.createImageData(width, height);
const destData = destImageData.data;
// Effect parameters calculation
const centerX = width / 2;
const centerY = height / 2;
// maxDist is the distance from the center to a corner, used for normalization.Ensures normalizedDist <= 1.
const maxDist = Math.hypot(centerX, centerY);
if (maxDist === 0) { // Handle 1x1 image case or similar where maxDist could be 0
outputCtx.putImageData(srcImageData, 0, 0); // No distortion possible, return original
return outputCanvas;
}
// Strength parameter controls the intensity of the radial pinch.
// Must be >= 1.0. strength < 1.0 would cause a bulge effect instead of a tunnel.
const effectiveStrength = Math.max(1.0, strength);
// Iterate over each pixel in the destination image
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const dXcen = x - centerX; // Delta X from center
const dYcen = y - centerY; // Delta Y from center
// Convert destination pixel's Cartesian coordinates to polar coordinates
const distFromCenter = Math.hypot(dXcen, dYcen); // Distance from center for current (x,y)
const angle = Math.atan2(dYcen, dXcen); // Angle for current (x,y)
// Normalize distance: 0 at center, 1 at corners (if maxDist is hypot to corner)
const normalizedDist = distFromCenter / maxDist;
// Apply radial distortion (tunnel pinch effect)
// The exponent `1.0 / effectiveStrength` determines the distortion:
// - If effectiveStrength = 1.0, exponent = 1.0, no change in radius.
// - If effectiveStrength > 1.0 (e.g., 2.0), exponent < 1.0 (e.g., 0.5).
// For `normalizedDist` between 0 and 1, `pow(normalizedDist, exponent)` will be >= `normalizedDist`.
// This means we sample from a larger radius in the source image, creating a pinch/zoom-out effect.
let newNormalizedDist = Math.pow(normalizedDist, 1.0 / effectiveStrength);
const sourceRadius = newNormalizedDist * maxDist;
// Apply angular distortion (swirl effect)
// The swirl effect increases with the distance from the center.
const swirlAngleOffset = swirl * normalizedDist; // Amount of swirl proportional to normalized distance
const sourceAngle = angle + swirlAngleOffset;
// Convert the modified polar coordinates (sourceRadius, sourceAngle) back to Cartesian coordinates
// These are relative to the center.
const sourceXRel = sourceRadius * Math.cos(sourceAngle);
const sourceYRel = sourceRadius * Math.sin(sourceAngle);
// Absolute source coordinates from which to sample the pixel
const sx = centerX + sourceXRel;
const sy = centerY + sourceYRel;
// Get the pixel color from the source image using bilinear interpolation
const pixelColor = getBilinearPixel(srcData, width, height, sx, sy);
// Set the destination pixel
const destIdx = (y * width + x) * 4;
destData[destIdx] = pixelColor[0]; // R
destData[destIdx + 1] = pixelColor[1]; // G
destData[destIdx + 2] = pixelColor[2]; // B
destData[destIdx + 3] = pixelColor[3]; // A
}
}
// Put the modified pixel data onto the output canvas
outputCtx.putImageData(destImageData, 0, 0);
return outputCanvas;
}
Apply Changes