You can edit the below JavaScript code to customize the image tool.
Apply Changes
async function processImage(originalImg, aberration = 10, centerExposure = 1.2, exposureRadius = 0.3, vignetteStrength = 0.7, vignetteFalloff = 2.0) {
const canvas = document.createElement('canvas');
// Using { willReadFrequently: true } can be an optimization hint for browsers
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const w = originalImg.naturalWidth || originalImg.width;
const h = originalImg.naturalHeight || originalImg.height;
canvas.width = w;
canvas.height = h;
ctx.drawImage(originalImg, 0, 0, w, h);
let sourceImageData;
try {
sourceImageData = ctx.getImageData(0, 0, w, h);
} catch (e) {
// This can happen if the image is cross-origin and the canvas becomes tainted.
console.error("Error getting ImageData: ", e);
// Depending on requirements, you might throw an error or return the original image drawn on canvas.
// For this exercise, we'll throw an error as processing isn't possible.
throw new Error("Could not get ImageData from canvas. Ensure the image is CORS-enabled if from a different origin.");
}
const sourceData = sourceImageData.data;
const outputImageData = ctx.createImageData(w, h);
const outputData = outputImageData.data;
const centerX = w / 2;
const centerY = h / 2;
// maxDist is the distance from the center to a corner, used for normalization
const maxDist = Math.sqrt(centerX * centerX + centerY * centerY);
// Bilinear interpolation helper function
// Samples a color from imageData at fractional coordinates (x, y)
function getPixelBilinear(data, imgWidth, imgHeight, x, y) {
// Clamp coordinates to be within the image bounds [0, width-1] and [0, height-1]
const x_clamped = Math.max(0, Math.min(x, imgWidth - 1));
const y_clamped = Math.max(0, Math.min(y, imgHeight - 1));
const x_floor = Math.floor(x_clamped);
const y_floor = Math.floor(y_clamped);
// Determine ceil coordinates, ensuring they don't exceed image boundaries
const x_ceil = Math.min(imgWidth - 1, x_floor + 1);
const y_ceil = Math.min(imgHeight - 1, y_floor + 1);
const fx = x_clamped - x_floor; // Fractional part of x
const fy = y_clamped - y_floor; // Fractional part of y
const fx1 = 1 - fx;
const fy1 = 1 - fy;
// Weights for the four neighboring pixels
const w1 = fx1 * fy1; // Weight for (x_floor, y_floor)
const w2 = fx * fy1; // Weight for (x_ceil, y_floor)
const w3 = fx1 * fy; // Weight for (x_floor, y_ceil)
const w4 = fx * fy; // Weight for (x_ceil, y_ceil)
// Indices of the four neighboring pixels in the imageData array
const idx1 = (y_floor * imgWidth + x_floor) * 4;
const idx2 = (y_floor * imgWidth + x_ceil) * 4;
const idx3 = (y_ceil * imgWidth + x_floor) * 4;
const idx4 = (y_ceil * imgWidth + x_ceil) * 4;
// Interpolate R, G, B, A channels
const r = data[idx1] * w1 + data[idx2] * w2 + data[idx3] * w3 + data[idx4] * w4;
const g = data[idx1+1] * w1 + data[idx2+1] * w2 + data[idx3+1] * w3 + data[idx4+1] * w4;
const b = data[idx1+2] * w1 + data[idx2+2] * w2 + data[idx3+2] * w3 + data[idx4+2] * w4;
const a = data[idx1+3] * w1 + data[idx2+3] * w2 + data[idx3+3] * w3 + data[idx4+3] * w4;
return [r, g, b, a];
}
for (let y_coord = 0; y_coord < h; y_coord++) {
for (let x_coord = 0; x_coord < w; x_coord++) {
const currentPixelIndex = (y_coord * w + x_coord) * 4;
const dx = x_coord - centerX;
const dy = y_coord - centerY;
const dist = Math.sqrt(dx * dx + dy * dy);
// Normalized distance from center (0 = center, 1 = corner)
// Avoid division by zero for extremely small images (e.g., 1x1 pixel)
const normDist = (maxDist === 0) ? 0 : (dist / maxDist);
// 1. Chromatic Aberration
// The amount of channel separation scales with distance from the center
const currentAberrationOffset = aberration * normDist;
let R_ab, G_ab, B_ab;
// Apply aberration if the effect is strong_ab enough and offset is noticeable
if (aberration > 0 && currentAberrationOffset > 0.01) {
const angle = Math.atan2(dy, dx); // Angle from center to current pixel
const cosAngle = Math.cos(angle);
const sinAngle = Math.sin(angle);
// Red channel shifted radially outwards
const rSampleX = x_coord + currentAberrationOffset * cosAngle;
const rSampleY = y_coord + currentAberrationOffset * sinAngle;
// Blue channel shifted radially inwards
const bSampleX = x_coord - currentAberrationOffset * cosAngle;
const bSampleY = y_coord - currentAberrationOffset * sinAngle;
// Sample R, G, B channels from their respective (potentially shifted) locations
// Green channel is sampled from the original pixel location for relative sharpness
const [rVal] = getPixelBilinear(sourceData, w, h, rSampleX, rSampleY);
const [gValFromSource] = getPixelBilinear(sourceData, w, h, x_coord, y_coord); // Get G from original position
const [_, __, bActualVal] = getPixelBilinear(sourceData, w, h, bSampleX, bSampleY); // Get B from its shifted position
R_ab = rVal;
G_ab = gValFromSource;
B_ab = bActualVal;
} else { // No significant aberration, use original pixel colors
R_ab = sourceData[currentPixelIndex];
G_ab = sourceData[currentPixelIndex + 1];
B_ab = sourceData[currentPixelIndex + 2];
}
const A_orig = sourceData[currentPixelIndex + 3]; // Preserve original alpha
// 2. Center Exposure Boost
let exposureFactor = 1.0;
// Apply brightness boost if centerExposure is not 1 (no change) and within exposureRadius
if (centerExposure !== 1.0 && exposureRadius > 0 && normDist < exposureRadius) {
// Linear falloff of exposure from `centerExposure` at the very center
// down to 1.0 (no change) at `exposureRadius`
exposureFactor = centerExposure - (centerExposure - 1.0) * (normDist / exposureRadius);
}
let R_exp = R_ab * exposureFactor;
let G_exp = G_ab * exposureFactor;
let B_exp = B_ab * exposureFactor;
// 3. Vignette
let vignetteMultiplier = 1.0;
if (vignetteStrength > 0) {
// `vignetteStrength` (0-1): 0 = no vignette, 1 = black edges.
// `vignetteFalloff`: controls how quickly the vignette effect transitions. Higher values mean sharper/faster falloff.
const vignetteEffectAmount = vignetteStrength * Math.pow(normDist, vignetteFalloff);
vignetteMultiplier = 1.0 - vignetteEffectAmount; // Factor to multiply color by
}
let R_final = R_exp * vignetteMultiplier;
let G_final = G_exp * vignetteMultiplier;
let B_final = B_exp * vignetteMultiplier;
// Write final clamped RGB values and original Alpha to output
outputData[currentPixelIndex] = Math.max(0, Math.min(255, R_final));
outputData[currentPixelIndex + 1] = Math.max(0, Math.min(255, G_final));
outputData[currentPixelIndex + 2] = Math.max(0, Math.min(255, B_final));
outputData[currentPixelIndex + 3] = A_orig;
}
}
ctx.putImageData(outputImageData, 0, 0);
return canvas;
}
Apply Changes