You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, strength = 0.5) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true }); // Optimization hint for getImageData heavy operations
const width = originalImg.naturalWidth || originalImg.width;
const height = originalImg.naturalHeight || originalImg.height;
canvas.width = width;
canvas.height = height;
if (width === 0 || height === 0) {
// Handles cases like unloaded or zero-size images
// Draw a (potentially empty) image if dimensions are zero, then return.
try {
ctx.drawImage(originalImg, 0, 0, width, height);
} catch (e) {
// Log error if originalImg is not drawable (e.g., broken)
console.error("Error drawing original image:", e);
}
return canvas;
}
// Draw the original image onto a temporary canvas to reliably get its pixel data.
const tempCanvas = document.createElement('canvas');
tempCanvas.width = width;
tempCanvas.height = height;
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
tempCtx.drawImage(originalImg, 0, 0, width, height);
const sourceImageData = tempCtx.getImageData(0, 0, width, height);
const sourceData = sourceImageData.data;
const outputImageData = ctx.createImageData(width, height);
const outputData = outputImageData.data;
const centerX = width / 2;
const centerY = height / 2;
// The lensRadius defines the radius of the fisheye effect's circular boundary.
// It's based on the largest inscribed circle in the image.
const lensRadius = Math.min(centerX, centerY);
// Clamping strength to be non-negative, as fisheye implies barrel distortion.
// Negative strength would result in pincushion distortion.
const effectStrength = Math.max(0, strength);
// Helper function for bilinear interpolation of pixel colors.
// This function forms a closure over sourceData, width, and height.
function getPixelBilinear(x, y) {
const x0 = Math.floor(x);
const y0 = Math.floor(y);
const x1 = x0 + 1;
const y1 = y0 + 1;
const xd = x - x0; // Fractional part of x
const yd = y - y0; // Fractional part of y
// Safely gets a pixel's RGBA array from sourceData, handling out-of-bounds.
const getPixel = (px, py) => {
if (px < 0 || px >= width || py < 0 || py >= height) {
return [0, 0, 0, 0]; // Transparent black for pixels outside the source image
}
const offset = (py * width + px) * 4;
return [
sourceData[offset], // R
sourceData[offset + 1], // G
sourceData[offset + 2], // B
sourceData[offset + 3] // A
];
};
const c00 = getPixel(x0, y0); // Color at (x0, y0) - top-left
const c10 = getPixel(x1, y0); // Color at (x1, y0) - top-right
const c01 = getPixel(x0, y1); // Color at (x0, y1) - bottom-left
const c11 = getPixel(x1, y1); // Color at (x1, y1) - bottom-right
const resultColor = [0, 0, 0, 0];
for (let i = 0; i < 4; i++) { // Iterate over R, G, B, A channels
// Interpolate horizontally for top row and bottom row
const topInterpolation = c00[i] * (1 - xd) + c10[i] * xd;
const bottomInterpolation = c01[i] * (1 - xd) + c11[i] * xd;
// Interpolate vertically between the two horizontal results
resultColor[i] = topInterpolation * (1 - yd) + bottomInterpolation * yd;
}
return resultColor;
}
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const idx = (y * width + x) * 4; // Current pixel's index in outputData
if (effectStrength === 0) {
// If strength is zero, no fisheye effect; copy original pixel.
// Using getPixelBilinear ensures sub-pixel accuracy if (x,y) were not integers,
// but here (x,y) are loop integers, so it effectively samples sourceData[idx].
const color = getPixelBilinear(x, y);
outputData[idx] = color[0];
outputData[idx + 1] = color[1];
outputData[idx + 2] = color[2];
outputData[idx + 3] = color[3];
continue;
}
// Calculate current pixel's offset from the image center
const dx_abs = x - centerX;
const dy_abs = y - centerY;
// Normalize coordinates based on lensRadius.
// (norm_dx_circ, norm_dy_circ) are coordinates where the edge of the lens is at radius 1.
const norm_dx_circ = (lensRadius === 0) ? 0 : dx_abs / lensRadius;
const norm_dy_circ = (lensRadius === 0) ? 0 : dy_abs / lensRadius;
// Calculate the distance of the destination pixel from the center in normalized lens coordinates.
const r_dest_norm = Math.sqrt(norm_dx_circ * norm_dx_circ + norm_dy_circ * norm_dy_circ);
if (r_dest_norm > 1.0) {
// Pixel is outside the circular fisheye lens area. Make it transparent black.
outputData[idx] = 0; // R
outputData[idx + 1] = 0; // G
outputData[idx + 2] = 0; // B
outputData[idx + 3] = 0; // A (transparent)
continue;
}
let srcX, srcY; // Coordinates of the source pixel to sample from
if (r_dest_norm === 0) {
// Center pixel maps to itself.
srcX = centerX;
srcY = centerY;
} else {
// Fisheye distortion formula:
// factor = r_src_norm / r_dest_norm
// This factor modifies the radius:
// - At center (r_dest_norm=0, handled by else block or r_dest_norm === 0 check): Magnification is (effectStrength + 1)
// - At lens edge (r_dest_norm=1): Magnification is (0.5 * effectStrength + 1)
// The center is magnified more than the edge, creating the bulge.
const factor = (-0.5 * effectStrength * r_dest_norm + effectStrength + 1.0);
// Calculate source normalized coordinates (still relative to lensRadius=1 system)
const src_norm_dx = norm_dx_circ * factor;
const src_norm_dy = norm_dy_circ * factor;
// Convert source normalized coordinates back to absolute pixel coordinates in the source image
const src_dx_abs = src_norm_dx * lensRadius;
const src_dy_abs = src_norm_dy * lensRadius;
srcX = centerX + src_dx_abs;
srcY = centerY + src_dy_abs;
}
// Get the color from the calculated source coordinates using bilinear interpolation.
const color = getPixelBilinear(srcX, srcY);
outputData[idx] = color[0];
outputData[idx + 1] = color[1];
outputData[idx + 2] = color[2];
outputData[idx + 3] = color[3];
}
}
// Put the processed pixel data onto the canvas.
ctx.putImageData(outputImageData, 0, 0);
return canvas;
}
Apply Changes