You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, centerXRatioInput = "0.5", centerYRatioInput = "0.5", strengthInput = "0.5", radiusRatioInput = "0.25") {
// Parse and validate parameters
let centerXRatio = parseFloat(centerXRatioInput);
if (isNaN(centerXRatio)) centerXRatio = 0.5;
let centerYRatio = parseFloat(centerYRatioInput);
if (isNaN(centerYRatio)) centerYRatio = 0.5;
let strength = parseFloat(strengthInput);
if (isNaN(strength)) strength = 0.5;
let radiusRatio = parseFloat(radiusRatioInput);
if (isNaN(radiusRatio)) radiusRatio = 0.25;
// Clamp strength: (1 + strength) must be > 0. Positive for bulge, negative for pinch.
// Strength > -1 is required for the Math.pow logic.
if (strength <= -1.0) strength = -0.99; // Max pinch, keeps power > 0
// Cap bulge strength to a practical maximum if desired, e.g., 5.0
if (strength > 5.0) strength = 5.0;
const canvas = document.createElement('canvas');
// Use willReadFrequently for potential performance gain when using getImageData repeatedly.
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const width = originalImg.naturalWidth || originalImg.width;
const height = originalImg.naturalHeight || originalImg.height;
canvas.width = width;
canvas.height = height;
ctx.drawImage(originalImg, 0, 0, width, height);
const imageData = ctx.getImageData(0, 0, width, height);
const outputImageData = ctx.createImageData(width, height);
const outputData = outputImageData.data;
const actualCenterX = width * centerXRatio;
const actualCenterY = height * centerYRatio;
// Base radius for the effect, relative to the smaller dimension of the image
const baseRadius = Math.min(width, height) * radiusRatio;
// Helper function to get a single pixel's color array [R, G, B, A] from image data
// Handles boundary clamping.
function getPixel(sourceImageData, x, y) {
const ix = Math.max(0, Math.min(sourceImageData.width - 1, Math.floor(x)));
const iy = Math.max(0, Math.min(sourceImageData.height - 1, Math.floor(y)));
const k = (iy * sourceImageData.width + ix) * 4;
return [
sourceImageData.data[k],
sourceImageData.data[k + 1],
sourceImageData.data[k + 2],
sourceImageData.data[k + 3]
];
}
// Helper function for bilinear interpolation
function getPixelBilinear(sourceImageData, x, y) {
const x_floor = Math.floor(x);
const y_floor = Math.floor(y);
const x_frac = x - x_floor;
const y_frac = y - y_floor;
const p00 = getPixel(sourceImageData, x_floor, y_floor);
const p10 = getPixel(sourceImageData, x_floor + 1, y_floor);
const p01 = getPixel(sourceImageData, x_floor, y_floor + 1);
const p11 = getPixel(sourceImageData, x_floor + 1, y_floor + 1);
const interpolatedPixel = [0, 0, 0, 0];
for (let i = 0; i < 4; i++) { // R, G, B, A channels
const c0 = p00[i] * (1 - x_frac) + p10[i] * x_frac; // Interpolate top row
const c1 = p01[i] * (1 - x_frac) + p11[i] * x_frac; // Interpolate bottom row
interpolatedPixel[i] = Math.round(c0 * (1 - y_frac) + c1 * y_frac); // Interpolate vertically
}
return interpolatedPixel;
}
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const dx = x - actualCenterX;
const dy = y - actualCenterY;
const distanceSquared = dx * dx + dy * dy;
let srcX = x;
let srcY = y;
// Apply effect only if baseRadius > 0, strength is non-zero, and pixel is within effect radius
if (baseRadius > 0 && strength !== 0 && distanceSquared < baseRadius * baseRadius) {
const distance = Math.sqrt(distanceSquared);
if (distance > 0) { // Avoid division by zero for center pixel
const r_dest_norm = distance / baseRadius; // Normalized distance (0 to 1)
// Power for distortion: (1 + strength). strength > 0 means bulge, strength < 0 means pinch.
const power_val = 1.0 + strength;
const r_src_norm = Math.pow(r_dest_norm, power_val);
const new_dist = r_src_norm * baseRadius;
// Calculate source coordinates by scaling along the vector from center
srcX = actualCenterX + (dx / distance) * new_dist;
srcY = actualCenterY + (dy / distance) * new_dist;
}
// If distance is 0 (center pixel), srcX/srcY remain x/y, so it maps to itself.
}
// If outside effect radius or no effect conditions, srcX/srcY remain x/y (original pixel).
const [r, g, b, a] = getPixelBilinear(imageData, srcX, srcY);
const destIndex = (y * width + x) * 4;
outputData[destIndex] = r;
outputData[destIndex + 1] = g;
outputData[destIndex + 2] = b;
outputData[destIndex + 3] = a;
}
}
ctx.putImageData(outputImageData, 0, 0);
return canvas;
}
Apply Changes