import type { FilterType, FilterParams } from '@/types/filter'; /** * Clamps a value between min and max */ function clamp(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max); } /** * Apply brightness adjustment to image data */ export function applyBrightness( imageData: ImageData, brightness: number ): ImageData { const data = imageData.data; const adjustment = (brightness / 100) * 255; for (let i = 0; i < data.length; i += 4) { data[i] = clamp(data[i] + adjustment, 0, 255); // R data[i + 1] = clamp(data[i + 1] + adjustment, 0, 255); // G data[i + 2] = clamp(data[i + 2] + adjustment, 0, 255); // B } return imageData; } /** * Apply contrast adjustment to image data */ export function applyContrast( imageData: ImageData, contrast: number ): ImageData { const data = imageData.data; const factor = (259 * (contrast + 255)) / (255 * (259 - contrast)); for (let i = 0; i < data.length; i += 4) { data[i] = clamp(factor * (data[i] - 128) + 128, 0, 255); // R data[i + 1] = clamp(factor * (data[i + 1] - 128) + 128, 0, 255); // G data[i + 2] = clamp(factor * (data[i + 2] - 128) + 128, 0, 255); // B } return imageData; } /** * Convert RGB to HSL */ function rgbToHsl( r: number, g: number, b: number ): [number, number, number] { r /= 255; g /= 255; b /= 255; const max = Math.max(r, g, b); const min = Math.min(r, g, b); const diff = max - min; let h = 0; let s = 0; const l = (max + min) / 2; if (diff !== 0) { s = l > 0.5 ? diff / (2 - max - min) : diff / (max + min); switch (max) { case r: h = ((g - b) / diff + (g < b ? 6 : 0)) / 6; break; case g: h = ((b - r) / diff + 2) / 6; break; case b: h = ((r - g) / diff + 4) / 6; break; } } return [h * 360, s * 100, l * 100]; } /** * Convert HSL to RGB */ function hslToRgb( h: number, s: number, l: number ): [number, number, number] { h /= 360; s /= 100; l /= 100; let r, g, b; if (s === 0) { r = g = b = l; } else { const hue2rgb = (p: number, q: number, t: number) => { if (t < 0) t += 1; if (t > 1) t -= 1; if (t < 1 / 6) return p + (q - p) * 6 * t; if (t < 1 / 2) return q; if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; return p; }; const q = l < 0.5 ? l * (1 + s) : l + s - l * s; const p = 2 * l - q; r = hue2rgb(p, q, h + 1 / 3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1 / 3); } return [r * 255, g * 255, b * 255]; } /** * Apply hue/saturation/lightness adjustment to image data */ export function applyHueSaturation( imageData: ImageData, hue: number, saturation: number, lightness: number ): ImageData { const data = imageData.data; const hueAdjust = hue; const satAdjust = saturation / 100; const lightAdjust = lightness / 100; for (let i = 0; i < data.length; i += 4) { const [h, s, l] = rgbToHsl(data[i], data[i + 1], data[i + 2]); const newH = (h + hueAdjust + 360) % 360; const newS = clamp(s + s * satAdjust, 0, 100); const newL = clamp(l + l * lightAdjust, 0, 100); const [r, g, b] = hslToRgb(newH, newS, newL); data[i] = clamp(r, 0, 255); data[i + 1] = clamp(g, 0, 255); data[i + 2] = clamp(b, 0, 255); } return imageData; } /** * Apply Gaussian blur to image data */ export function applyBlur(imageData: ImageData, radius: number): ImageData { const width = imageData.width; const height = imageData.height; const data = imageData.data; // Create kernel const kernelSize = Math.ceil(radius) * 2 + 1; const kernel: number[] = []; let kernelSum = 0; for (let i = 0; i < kernelSize; i++) { const x = i - Math.floor(kernelSize / 2); const value = Math.exp(-(x * x) / (2 * radius * radius)); kernel.push(value); kernelSum += value; } // Normalize kernel for (let i = 0; i < kernel.length; i++) { kernel[i] /= kernelSum; } // Temporary buffer const tempData = new Uint8ClampedArray(data.length); tempData.set(data); // 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 < kernelSize; k++) { const offsetX = x + k - Math.floor(kernelSize / 2); if (offsetX >= 0 && offsetX < width) { const idx = (y * width + offsetX) * 4; const weight = kernel[k]; r += tempData[idx] * weight; g += tempData[idx + 1] * weight; b += tempData[idx + 2] * weight; a += tempData[idx + 3] * weight; } } const idx = (y * width + x) * 4; data[idx] = r; data[idx + 1] = g; data[idx + 2] = b; data[idx + 3] = a; } } // Copy for vertical pass tempData.set(data); // Vertical 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 < kernelSize; k++) { const offsetY = y + k - Math.floor(kernelSize / 2); if (offsetY >= 0 && offsetY < height) { const idx = (offsetY * width + x) * 4; const weight = kernel[k]; r += tempData[idx] * weight; g += tempData[idx + 1] * weight; b += tempData[idx + 2] * weight; a += tempData[idx + 3] * weight; } } const idx = (y * width + x) * 4; data[idx] = r; data[idx + 1] = g; data[idx + 2] = b; data[idx + 3] = a; } } return imageData; } /** * Apply sharpening filter to image data */ export function applySharpen(imageData: ImageData, amount: number): ImageData { const width = imageData.width; const height = imageData.height; const data = imageData.data; const tempData = new Uint8ClampedArray(data.length); tempData.set(data); const factor = amount / 100; const kernel = [ [0, -factor, 0], [-factor, 1 + 4 * factor, -factor], [0, -factor, 0], ]; for (let y = 1; y < height - 1; y++) { for (let x = 1; x < width - 1; x++) { let r = 0, g = 0, b = 0; for (let ky = -1; ky <= 1; ky++) { for (let kx = -1; kx <= 1; kx++) { const idx = ((y + ky) * width + (x + kx)) * 4; const weight = kernel[ky + 1][kx + 1]; r += tempData[idx] * weight; g += tempData[idx + 1] * weight; b += tempData[idx + 2] * weight; } } const idx = (y * width + x) * 4; data[idx] = clamp(r, 0, 255); data[idx + 1] = clamp(g, 0, 255); data[idx + 2] = clamp(b, 0, 255); } } return imageData; } /** * Apply invert filter to image data */ export function applyInvert(imageData: ImageData): ImageData { const data = imageData.data; for (let i = 0; i < data.length; i += 4) { data[i] = 255 - data[i]; // R data[i + 1] = 255 - data[i + 1]; // G data[i + 2] = 255 - data[i + 2]; // B } return imageData; } /** * Apply grayscale filter to image data */ export function applyGrayscale(imageData: ImageData): ImageData { const data = imageData.data; for (let i = 0; i < data.length; i += 4) { const gray = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114; data[i] = gray; // R data[i + 1] = gray; // G data[i + 2] = gray; // B } return imageData; } /** * Apply sepia filter to image data */ export function applySepia(imageData: ImageData): ImageData { const data = imageData.data; for (let i = 0; i < data.length; i += 4) { const r = data[i]; const g = data[i + 1]; const b = data[i + 2]; data[i] = clamp(r * 0.393 + g * 0.769 + b * 0.189, 0, 255); data[i + 1] = clamp(r * 0.349 + g * 0.686 + b * 0.168, 0, 255); data[i + 2] = clamp(r * 0.272 + g * 0.534 + b * 0.131, 0, 255); } return imageData; } /** * Apply threshold filter to image data */ export function applyThreshold( imageData: ImageData, threshold: number ): ImageData { const data = imageData.data; for (let i = 0; i < data.length; i += 4) { const gray = data[i] * 0.299 + data[i + 1] * 0.587 + data[i + 2] * 0.114; const value = gray >= threshold ? 255 : 0; data[i] = value; data[i + 1] = value; data[i + 2] = value; } return imageData; } /** * Apply posterize filter to image data */ export function applyPosterize(imageData: ImageData, levels: number): ImageData { const data = imageData.data; const step = 255 / (levels - 1); for (let i = 0; i < data.length; i += 4) { data[i] = Math.round(data[i] / step) * step; data[i + 1] = Math.round(data[i + 1] / step) * step; data[i + 2] = Math.round(data[i + 2] / step) * step; } return imageData; } /** * Apply a filter to image data based on type and parameters */ export function applyFilter( imageData: ImageData, type: FilterType, params: FilterParams ): ImageData { // Clone the image data to avoid modifying the original const clonedData = new ImageData( new Uint8ClampedArray(imageData.data), imageData.width, imageData.height ); switch (type) { case 'brightness': return applyBrightness(clonedData, params.brightness ?? 0); case 'contrast': return applyContrast(clonedData, params.contrast ?? 0); case 'hue-saturation': return applyHueSaturation( clonedData, params.hue ?? 0, params.saturation ?? 0, params.lightness ?? 0 ); case 'blur': return applyBlur(clonedData, params.radius ?? 5); case 'sharpen': return applySharpen(clonedData, params.amount ?? 50); case 'invert': return applyInvert(clonedData); case 'grayscale': return applyGrayscale(clonedData); case 'sepia': return applySepia(clonedData); case 'threshold': return applyThreshold(clonedData, params.threshold ?? 128); case 'posterize': return applyPosterize(clonedData, params.levels ?? 8); default: return clonedData; } }