You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, twistStr = "30", bulgeStr = "0.15", radiusFactorStr = "0.75", centerXFactorStr = "0.5", centerYFactorStr = "0.5") {
const twistAngleDegrees = parseFloat(twistStr);
const bulgeAmount = parseFloat(bulgeStr); // Positive for bloat/bulge, negative for pinch/pucker
const radiusFactor = parseFloat(radiusFactorStr);
const centerXFactor = parseFloat(centerXFactorStr);
const centerYFactor = parseFloat(centerYFactorStr);
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const width = originalImg.width;
const height = originalImg.height;
if (width === 0 || height === 0) {
canvas.width = 0;
canvas.height = 0;
return canvas;
}
canvas.width = width;
canvas.height = height;
// Draw the original image to a source canvas to get its pixel data
const srcCanvas = document.createElement('canvas');
srcCanvas.width = width;
srcCanvas.height = height;
const srcCtx = srcCanvas.getContext('2d', { willReadFrequently: true });
srcCtx.drawImage(originalImg, 0, 0, width, height);
const srcImageData = srcCtx.getImageData(0, 0, width, height);
const srcData = srcImageData.data;
const destImageData = ctx.createImageData(width, height);
const destData = destImageData.data;
const actualCenterX = width * centerXFactor;
const actualCenterY = height * centerYFactor;
const actualEffectRadius = Math.min(width, height) / 2 * radiusFactor;
const twistAngleRad = twistAngleDegrees * Math.PI / 180;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const dx = x - actualCenterX; // Delta x from center
const dy = y - actualCenterY; // Delta y from center
const dist = Math.sqrt(dx * dx + dy * dy); // Distance from center
const angle = Math.atan2(dy, dx); // Angle from center
let srcX, srcY;
if (dist < actualEffectRadius && actualEffectRadius > 0) {
let normalized_dist = dist / actualEffectRadius; // Normalized distance [0, 1)
// 1. Inverse Bulge/Pinch transformation
// Calculates the source radius before bulge/pinch was applied
let radius_after_unbulge_normalized;
const power = 1.0 - bulgeAmount;
if (normalized_dist === 0) { // Pixel is exactly at the center
radius_after_unbulge_normalized = 0;
} else if (Math.abs(power) < 0.00001) { // bulgeAmount is very close to 1.0 (power is ~0)
// For bulge ~1 (max bloat), source points are pulled from the center
radius_after_unbulge_normalized = (normalized_dist < 1.0) ? 0.0 : 1.0; // Effectively 0 for interior points
} else {
const unBulgePower = 1.0 / power;
radius_after_unbulge_normalized = Math.pow(normalized_dist, unBulgePower);
}
// Safety check for results from Math.pow if extreme values were used
if (isNaN(radius_after_unbulge_normalized) || !isFinite(radius_after_unbulge_normalized)) {
// Fallback: if normalized_dist was 0, result is 0. Otherwise, could be from pow(negative, non-integer) or pow(0, negative)
// Default to a state that implies minimal distortion or center mapping
radius_after_unbulge_normalized = (normalized_dist === 0 || normalized_dist < 1.0 && bulgeAmount >=1.0) ? 0.0 : normalized_dist;
}
const radius_after_unbulge = radius_after_unbulge_normalized * actualEffectRadius;
// 2. Inverse Twist transformation
// Applied to the coordinates obtained after un-bulging/un-pinching
const dist_for_swirl = radius_after_unbulge;
let normalized_dist_for_swirl = (actualEffectRadius === 0) ? 0 : dist_for_swirl / actualEffectRadius;
// Clamp normalized_dist_for_swirl to [0,1] to keep swirl factor sensible
// This is important if un-bulge produces a radius larger than actualEffectRadius
normalized_dist_for_swirl = Math.max(0.0, Math.min(1.0, normalized_dist_for_swirl));
// Standard swirl falloff: effect is strongest at center, zero at radius edge
const swirlEffectFactor = 1.0 - normalized_dist_for_swirl;
// Alternative: swirl strongest at edge: const swirlEffectFactor = normalized_dist_for_swirl;
const angle_offset = twistAngleRad * swirlEffectFactor;
const final_src_angle = angle - angle_offset; // Subtract for inverse mapping (destination to source)
const srcX_relative = dist_for_swirl * Math.cos(final_src_angle);
const srcY_relative = dist_for_swirl * Math.sin(final_src_angle);
srcX = actualCenterX + srcX_relative;
srcY = actualCenterY + srcY_relative;
} else { // Outside effect radius, or radius is zero
srcX = x;
srcY = y;
}
const destIndex = (y * width + x) * 4;
// Final safety check for srcX, srcY before sampling
if (isNaN(srcX) || isNaN(srcY) || !isFinite(srcX) || !isFinite(srcY)) {
// Copy original pixel (or set to transparent black) if calculations led to invalid coords
const origIndex = (y * width + x) * 4;
destData[destIndex] = srcData[origIndex];
destData[destIndex + 1] = srcData[origIndex + 1];
destData[destIndex + 2] = srcData[origIndex + 2];
destData[destIndex + 3] = srcData[origIndex + 3];
} else {
// Bilinear Interpolation for smoother results
const clampedSrcX = Math.max(0, Math.min(width - 1, srcX));
const clampedSrcY = Math.max(0, Math.min(height - 1, srcY));
const sx_f = Math.floor(clampedSrcX); // Source X floor
const sy_f = Math.floor(clampedSrcY); // Source Y floor
// Ensure ceiling doesn't exceed image boundaries
const sx_c = Math.min(width - 1, sx_f + 1); // Source X ceil
const sy_c = Math.min(height - 1, sy_f + 1); // Source Y ceil
const u = clampedSrcX - sx_f; // Fractional part for x
const v = clampedSrcY - sy_f; // Fractional part for y
for (let i = 0; i < 4; i++) { // R, G, B, A channels
const cTL = srcData[(sy_f * width + sx_f) * 4 + i]; // Top-left pixel
const cTR = srcData[(sy_f * width + sx_c) * 4 + i]; // Top-right pixel
const cBL = srcData[(sy_c * width + sx_f) * 4 + i]; // Bottom-left pixel
const cBR = srcData[(sy_c * width + sx_c) * 4 + i]; // Bottom-right pixel
// Interpolate top row, then bottom row, then vertically between them
const topInterpolation = cTL * (1 - u) + cTR * u;
const bottomInterpolation = cBL * (1 - u) + cBR * u;
destData[destIndex + i] = Math.round(topInterpolation * (1 - v) + bottomInterpolation * v);
}
}
}
}
ctx.putImageData(destImageData, 0, 0);
return canvas;
}
Apply Changes