filters/gaussianBlur.js

/**
 * @fileoverview LuminaJS Filters - Gaussian Blur
 * Applies a smooth Gaussian blur effect to an image.
 */

/**
 * Applies a Gaussian blur filter to a copy of the provided `ImageData`.
 * This implementation uses a two-pass separable convolution for better performance.
 *
 * Performance notes:
 * - Runtime increases with image size and sigma.
 * - Sigma also increases kernel radius (`ceil(sigma * 3)`), which raises compute cost.
 * - On large images this can block the main thread; prefer preview resizing and/or Web Worker offload.
 *
 * @param {ImageData} imageData - The source pixel data.
 * @param {number} sigma - The standard deviation of the Gaussian distribution.
 *   Larger values result in more blurring. Default is 2.
 * @returns {ImageData} A new `ImageData` object with Gaussian blur applied.
 *
 * @example
 * const blurredData = gaussianBlur(imageData, 3.5);
 */
export function gaussianBlur(imageData, sigma = 2) {
  const width = imageData.width;
  const height = imageData.height;
  const input = imageData.data;
  const output = new Uint8ClampedArray(input.length);
  const temp = new Uint8ClampedArray(input.length);

  if (sigma <= 0) {
    return new ImageData(new Uint8ClampedArray(input), width, height);
  }

  // Calculate kernel radius (3 * sigma is standard for Gaussian)
  const radius = Math.ceil(sigma * 3);
  const size = radius * 2 + 1;
  const kernel = new Float32Array(size);

  // Pre-calculate Gaussian kernel
  let sum = 0;
  for (let i = 0; i < size; i++) {
    const x = i - radius;
    kernel[i] = Math.exp(-(x * x) / (2 * sigma * sigma));
    sum += kernel[i];
  }
  // Normalize kernel
  for (let i = 0; i < size; i++) {
    kernel[i] /= sum;
  }

  // Horizontal pass
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      let r = 0,
        g = 0,
        b = 0,
        a = 0;
      for (let k = 0; k < size; k++) {
        const nx = x + (k - radius);
        // Edge handling: clamping to nearest pixel
        const ix = Math.max(0, Math.min(width - 1, nx));
        const offset = (y * width + ix) * 4;
        const weight = kernel[k];

        r += input[offset] * weight;
        g += input[offset + 1] * weight;
        b += input[offset + 2] * weight;
        a += input[offset + 3] * weight;
      }
      const offset = (y * width + x) * 4;
      temp[offset] = r;
      temp[offset + 1] = g;
      temp[offset + 2] = b;
      temp[offset + 3] = a;
    }
  }

  // Vertical pass
  for (let x = 0; x < width; x++) {
    for (let y = 0; y < height; y++) {
      let r = 0,
        g = 0,
        b = 0,
        a = 0;
      for (let k = 0; k < size; k++) {
        const ny = y + (k - radius);
        const iy = Math.max(0, Math.min(height - 1, ny));
        const offset = (iy * width + x) * 4;
        const weight = kernel[k];

        r += temp[offset] * weight;
        g += temp[offset + 1] * weight;
        b += temp[offset + 2] * weight;
        a += temp[offset + 3] * weight;
      }
      const offset = (y * width + x) * 4;
      output[offset] = r;
      output[offset + 1] = g;
      output[offset + 2] = b;
      output[offset + 3] = a;
    }
  }

  return new ImageData(output, width, height);
}