You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, centerXParam, centerYParam, radiusParam, strengthParam, directionAngleParam) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// Use naturalWidth/Height for intrinsic image dimensions
const w = originalImg.naturalWidth || originalImg.width;
const h = originalImg.naturalHeight || originalImg.height;
if (w === 0 || h === 0) {
// Handle cases like image not loaded or 0x0 image
console.warn("Image has zero width or height. Cannot apply liquify filter.");
canvas.width = w || 1; // Avoid 0-size canvas if problematic
canvas.height = h || 1;
// Optionally draw the (empty or tiny) original image if it makes sense
if (w > 0 && h > 0) {
try {
ctx.drawImage(originalImg, 0, 0, w, h);
} catch (e) {
// Ignore if it cannot be drawn
}
}
return canvas;
}
canvas.width = w;
canvas.height = h;
// Helper function for parsing numeric parameters with defaults
const parseNumericParam = (param, defaultValue) => {
if (param === undefined || param === null) {
return defaultValue;
}
const strParam = String(param).trim();
if (strParam === "") {
return defaultValue;
}
const num = Number(strParam);
return isNaN(num) ? defaultValue : num;
};
const centerX = parseNumericParam(centerXParam, w / 2);
const centerY = parseNumericParam(centerYParam, h / 2);
let radius = parseNumericParam(radiusParam, Math.min(w, h) / 4);
if (radius <= 0) { // Ensure radius is positive
radius = Math.min(w, h) / 4; // Try default again
if (radius <= 0) radius = Math.max(w,h) > 0 ? 1 : 0; // Absolute fallback if min(w,h) is 0 but not w and h
}
// Strength can be 0 (no effect) or negative (opposite push).
const strength = parseNumericParam(strengthParam, radius / 3);
const directionAngle = parseNumericParam(directionAngleParam, 0); // Default angle 0 (to the right)
// Draw original image to a temporary canvas to get its pixel data
// Using willReadFrequently hint for potential performance improvement
const tempCanvas = document.createElement('canvas');
tempCanvas.width = w;
tempCanvas.height = h;
const tempCtx = tempCanvas.getContext('2d', { willReadFrequently: true });
try {
tempCtx.drawImage(originalImg, 0, 0, w, h);
} catch (e) {
console.error("Error drawing original image to temporary canvas: ", e);
// Fallback: return original image drawn on the main canvas
ctx.drawImage(originalImg, 0, 0, w, h);
return canvas;
}
let originalImageData;
try {
originalImageData = tempCtx.getImageData(0, 0, w, h);
} catch (e) {
console.error("Error getting image data for liquify filter (possibly CORS issue): ", e);
// Fallback: return original image drawn on the main canvas
ctx.drawImage(originalImg, 0, 0, w, h);
return canvas;
}
const originalPixels = originalImageData.data;
const outputImageData = ctx.createImageData(w, h); // Use main canvas context to create ImageData
const outputPixels = outputImageData.data;
const angleRad = directionAngle * (Math.PI / 180);
const pushDirX = Math.cos(angleRad);
const pushDirY = Math.sin(angleRad);
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const currentPixelIndex = (y * w + x) * 4;
const dXFromCenter = x - centerX;
const dYFromCenter = y - centerY;
const distanceFromCenter = Math.sqrt(dXFromCenter * dXFromCenter + dYFromCenter * dYFromCenter);
let sourceX = x;
let sourceY = y;
if (distanceFromCenter < radius && radius > 0) { // Apply effect only within radius
// Quadratic falloff: (1 - dist/radius)^2 for smoother effect
let falloff = (radius - distanceFromCenter) / radius;
falloff = falloff * falloff;
const displacement = strength * falloff;
// Calculate source pixel by displacing against the push direction
sourceX = x - displacement * pushDirX;
sourceY = y - displacement * pushDirY;
}
// Clamp source coordinates to be within the image bounds
const sX_clamped = Math.max(0, Math.min(w - 1, sourceX));
const sY_clamped = Math.max(0, Math.min(h - 1, sourceY));
// Bilinear interpolation for smoother results
const x0 = Math.floor(sX_clamped);
const y0 = Math.floor(sY_clamped);
// Determine coordinates of the pixel to the right/bottom of (x0,y0), ensuring they stay within bounds.
const x1 = Math.min(x0 + 1, w - 1);
const y1 = Math.min(y0 + 1, h - 1);
// Fractional parts for interpolation weights
const fX = sX_clamped - x0; // Weight for x1 contribution
const fY = sY_clamped - y0; // Weight for y1 contribution
// Indices for the four neighboring pixels in the originalPixels array
const p00_idx = (y0 * w + x0) * 4; // Top-left pixel (x0,y0)
const p10_idx = (y0 * w + x1) * 4; // Top-right pixel (x1,y0)
const p01_idx = (y1 * w + x0) * 4; // Bottom-left pixel (x0,y1)
const p11_idx = (y1 * w + x1) * 4; // Bottom-right pixel (x1,y1)
// Interpolate RGBA channels
for (let channel = 0; channel < 4; channel++) {
const C00 = originalPixels[p00_idx + channel];
const C10 = originalPixels[p10_idx + channel];
const C01 = originalPixels[p01_idx + channel];
const C11 = originalPixels[p11_idx + channel];
// Interpolate horizontally for top row: (x0,y0) and (x1,y0)
const topInterpolation = C00 * (1 - fX) + C10 * fX;
// Interpolate horizontally for bottom row: (x0,y1) and (x1,y1)
const bottomInterpolation = C01 * (1 - fX) + C11 * fX;
// Interpolate vertically between the results of horizontal interpolations
const interpolatedValue = topInterpolation * (1 - fY) + bottomInterpolation * fY;
outputPixels[currentPixelIndex + channel] = interpolatedValue;
}
}
}
ctx.putImageData(outputImageData, 0, 0);
return canvas;
}
Apply Changes