diff --git a/components/editor/editor-layout.tsx b/components/editor/editor-layout.tsx
index 637ba60..224860a 100644
--- a/components/editor/editor-layout.tsx
+++ b/components/editor/editor-layout.tsx
@@ -9,6 +9,7 @@ import { HistoryPanel } from './history-panel';
import { FileMenu } from './file-menu';
import { ToolPalette, ToolSettings } from '@/components/tools';
import { ColorPanel } from '@/components/colors';
+import { FilterPanel } from '@/components/filters';
import { useKeyboardShortcuts } from '@/hooks/use-keyboard-shortcuts';
import { useFileOperations } from '@/hooks/use-file-operations';
import { useDragDrop } from '@/hooks/use-drag-drop';
@@ -175,6 +176,9 @@ export function EditorLayout() {
{/* Color Panel */}
+ {/* Filter Panel */}
+
+
{/* Canvas area */}
diff --git a/components/filters/filter-panel.tsx b/components/filters/filter-panel.tsx
new file mode 100644
index 0000000..9b4f6d1
--- /dev/null
+++ b/components/filters/filter-panel.tsx
@@ -0,0 +1,361 @@
+'use client';
+
+import { useState } from 'react';
+import { useFilterStore } from '@/store/filter-store';
+import { useLayerStore } from '@/store/layer-store';
+import { useHistoryStore } from '@/store/history-store';
+import { useFilterPreview } from '@/hooks/use-filter-preview';
+import { FilterCommand } from '@/core/commands/filter-command';
+import type { FilterType } from '@/types/filter';
+import {
+ Wand2,
+ Sun,
+ SunMoon,
+ Palette,
+ Droplet,
+ Sparkles,
+ Slash,
+ Paintbrush,
+ Circle,
+ Grid3x3,
+ Eye,
+ Check,
+ X,
+} from 'lucide-react';
+import { cn } from '@/lib/utils';
+
+const FILTERS: Array<{
+ type: FilterType;
+ label: string;
+ icon: React.ComponentType<{ className?: string }>;
+ hasParams: boolean;
+}> = [
+ { type: 'brightness', label: 'Brightness', icon: Sun, hasParams: true },
+ { type: 'contrast', label: 'Contrast', icon: SunMoon, hasParams: true },
+ {
+ type: 'hue-saturation',
+ label: 'Hue/Saturation',
+ icon: Palette,
+ hasParams: true,
+ },
+ { type: 'blur', label: 'Blur', icon: Droplet, hasParams: true },
+ { type: 'sharpen', label: 'Sharpen', icon: Sparkles, hasParams: true },
+ { type: 'invert', label: 'Invert', icon: Slash, hasParams: false },
+ { type: 'grayscale', label: 'Grayscale', icon: Paintbrush, hasParams: false },
+ { type: 'sepia', label: 'Sepia', icon: Circle, hasParams: false },
+ { type: 'threshold', label: 'Threshold', icon: Grid3x3, hasParams: true },
+ { type: 'posterize', label: 'Posterize', icon: Grid3x3, hasParams: true },
+];
+
+export function FilterPanel() {
+ const {
+ activeFilter,
+ params,
+ setActiveFilter,
+ updateParams,
+ resetParams,
+ isPreviewMode,
+ setPreviewMode,
+ } = useFilterStore();
+ const { activeLayerId, layers } = useLayerStore();
+ const { executeCommand } = useHistoryStore();
+ const [selectedFilter, setSelectedFilter] = useState
(null);
+
+ useFilterPreview();
+
+ const activeLayer = layers.find((l) => l.id === activeLayerId);
+ const hasActiveLayer = !!activeLayer && !activeLayer.locked;
+
+ const handleFilterSelect = (filterType: FilterType) => {
+ const filter = FILTERS.find((f) => f.type === filterType);
+ if (!filter) return;
+
+ if (filter.hasParams) {
+ setSelectedFilter(filterType);
+ setActiveFilter(filterType);
+ resetParams();
+ } else {
+ // Apply filter immediately for filters without parameters
+ if (activeLayer) {
+ const command = FilterCommand.applyToLayer(activeLayer, filterType, {});
+ executeCommand(command);
+ }
+ }
+ };
+
+ const handleApply = () => {
+ if (activeFilter && activeLayer) {
+ setPreviewMode(false);
+ const command = FilterCommand.applyToLayer(
+ activeLayer,
+ activeFilter,
+ params
+ );
+ executeCommand(command);
+ setActiveFilter(null);
+ setSelectedFilter(null);
+ }
+ };
+
+ const handleCancel = () => {
+ setPreviewMode(false);
+ setActiveFilter(null);
+ setSelectedFilter(null);
+ resetParams();
+ };
+
+ const handlePreviewToggle = () => {
+ setPreviewMode(!isPreviewMode);
+ };
+
+ return (
+
+ {/* Header */}
+
+
+
Filters
+
+
+ {/* Filter list */}
+
+
+ {FILTERS.map((filter) => (
+
+ ))}
+
+
+ {/* Filter parameters */}
+ {selectedFilter && activeFilter && (
+
+
+ Parameters
+
+
+ {activeFilter === 'brightness' && (
+
+
+
+ updateParams({ brightness: Number(e.target.value) })
+ }
+ className="w-full"
+ />
+
+ {params.brightness ?? 0}
+
+
+ )}
+
+ {activeFilter === 'contrast' && (
+
+
+
+ updateParams({ contrast: Number(e.target.value) })
+ }
+ className="w-full"
+ />
+
+ {params.contrast ?? 0}
+
+
+ )}
+
+ {activeFilter === 'hue-saturation' && (
+ <>
+
+
+
+ updateParams({ hue: Number(e.target.value) })
+ }
+ className="w-full"
+ />
+
+ {params.hue ?? 0}°
+
+
+
+
+
+
+ updateParams({ saturation: Number(e.target.value) })
+ }
+ className="w-full"
+ />
+
+ {params.saturation ?? 0}%
+
+
+
+
+
+
+ updateParams({ lightness: Number(e.target.value) })
+ }
+ className="w-full"
+ />
+
+ {params.lightness ?? 0}%
+
+
+ >
+ )}
+
+ {activeFilter === 'blur' && (
+
+
+
+ updateParams({ radius: Number(e.target.value) })
+ }
+ className="w-full"
+ />
+
+ {params.radius ?? 5}px
+
+
+ )}
+
+ {activeFilter === 'sharpen' && (
+
+
+
+ updateParams({ amount: Number(e.target.value) })
+ }
+ className="w-full"
+ />
+
+ {params.amount ?? 50}%
+
+
+ )}
+
+ {activeFilter === 'threshold' && (
+
+
+
+ updateParams({ threshold: Number(e.target.value) })
+ }
+ className="w-full"
+ />
+
+ {params.threshold ?? 128}
+
+
+ )}
+
+ {activeFilter === 'posterize' && (
+
+
+
+ updateParams({ levels: Number(e.target.value) })
+ }
+ className="w-full"
+ />
+
+ {params.levels ?? 8}
+
+
+ )}
+
+ {/* Preview toggle */}
+
+
+ {/* Action buttons */}
+
+
+
+
+
+ )}
+
+
+ {!hasActiveLayer && (
+
+
+ Select an unlocked layer to apply filters
+
+
+ )}
+
+ );
+}
diff --git a/components/filters/index.ts b/components/filters/index.ts
new file mode 100644
index 0000000..db6b021
--- /dev/null
+++ b/components/filters/index.ts
@@ -0,0 +1 @@
+export * from './filter-panel';
diff --git a/core/commands/filter-command.ts b/core/commands/filter-command.ts
new file mode 100644
index 0000000..74e0b01
--- /dev/null
+++ b/core/commands/filter-command.ts
@@ -0,0 +1,105 @@
+import { BaseCommand } from './base-command';
+import type { Layer, FilterType, FilterParams } from '@/types';
+import { applyFilter } from '@/lib/filter-utils';
+import { cloneCanvas } from '@/lib/canvas-utils';
+
+export class FilterCommand extends BaseCommand {
+ private layerId: string;
+ private filterType: FilterType;
+ private filterParams: FilterParams;
+ private beforeCanvas: HTMLCanvasElement | null = null;
+ private afterCanvas: HTMLCanvasElement | null = null;
+
+ constructor(
+ layer: Layer,
+ filterType: FilterType,
+ filterParams: FilterParams
+ ) {
+ super(`Apply ${filterType} filter`);
+ this.layerId = layer.id;
+ this.filterType = filterType;
+ this.filterParams = filterParams;
+
+ // Capture the before state
+ if (layer.canvas) {
+ this.beforeCanvas = cloneCanvas(layer.canvas);
+ }
+ }
+
+ /**
+ * Capture the state after applying the filter
+ */
+ captureAfterState(layer: Layer): void {
+ if (layer.canvas) {
+ this.afterCanvas = cloneCanvas(layer.canvas);
+ }
+ }
+
+ execute(): void {
+ // Restore the after state
+ if (this.afterCanvas) {
+ const layer = this.getLayer();
+ if (layer?.canvas) {
+ const ctx = layer.canvas.getContext('2d');
+ if (ctx) {
+ ctx.clearRect(0, 0, layer.canvas.width, layer.canvas.height);
+ ctx.drawImage(this.afterCanvas, 0, 0);
+ }
+ }
+ }
+ }
+
+ undo(): void {
+ // Restore the before state
+ if (this.beforeCanvas) {
+ const layer = this.getLayer();
+ if (layer?.canvas) {
+ const ctx = layer.canvas.getContext('2d');
+ if (ctx) {
+ ctx.clearRect(0, 0, layer.canvas.width, layer.canvas.height);
+ ctx.drawImage(this.beforeCanvas, 0, 0);
+ }
+ }
+ }
+ }
+
+ private getLayer(): Layer | undefined {
+ const { useLayerStore } = require('@/store/layer-store');
+ const { layers } = useLayerStore.getState();
+ return layers.find((l: Layer) => l.id === this.layerId);
+ }
+
+ /**
+ * Apply the filter to a layer and return the command
+ */
+ static applyToLayer(
+ layer: Layer,
+ filterType: FilterType,
+ filterParams: FilterParams
+ ): FilterCommand {
+ const command = new FilterCommand(layer, filterType, filterParams);
+
+ // Apply the filter
+ if (layer.canvas) {
+ const ctx = layer.canvas.getContext('2d');
+ if (ctx) {
+ const imageData = ctx.getImageData(
+ 0,
+ 0,
+ layer.canvas.width,
+ layer.canvas.height
+ );
+ const filteredData = applyFilter(imageData, filterType, filterParams);
+ ctx.putImageData(filteredData, 0, 0);
+
+ // Update the layer's updatedAt timestamp
+ const { useLayerStore } = require('@/store/layer-store');
+ const { updateLayer } = useLayerStore.getState();
+ updateLayer(layer.id, { updatedAt: Date.now() });
+ }
+ }
+
+ command.captureAfterState(layer);
+ return command;
+ }
+}
diff --git a/hooks/use-filter-preview.ts b/hooks/use-filter-preview.ts
new file mode 100644
index 0000000..512e7b1
--- /dev/null
+++ b/hooks/use-filter-preview.ts
@@ -0,0 +1,79 @@
+import { useEffect, useRef } from 'react';
+import { useFilterStore } from '@/store/filter-store';
+import { useLayerStore } from '@/store/layer-store';
+import { applyFilter } from '@/lib/filter-utils';
+import { cloneCanvas } from '@/lib/canvas-utils';
+
+/**
+ * Hook for previewing filters on the active layer
+ */
+export function useFilterPreview() {
+ const { activeFilter, params, isPreviewMode } = useFilterStore();
+ const { activeLayerId, layers } = useLayerStore();
+ const originalCanvasRef = useRef(null);
+
+ const activeLayer = layers.find((l) => l.id === activeLayerId);
+
+ // Store original canvas when preview starts
+ useEffect(() => {
+ if (isPreviewMode && activeLayer?.canvas && !originalCanvasRef.current) {
+ originalCanvasRef.current = cloneCanvas(activeLayer.canvas);
+ }
+
+ if (!isPreviewMode && originalCanvasRef.current) {
+ // Restore original when preview is disabled
+ if (activeLayer?.canvas) {
+ const ctx = activeLayer.canvas.getContext('2d');
+ if (ctx) {
+ ctx.clearRect(0, 0, activeLayer.canvas.width, activeLayer.canvas.height);
+ ctx.drawImage(originalCanvasRef.current, 0, 0);
+ }
+ }
+ originalCanvasRef.current = null;
+ }
+ }, [isPreviewMode, activeLayer]);
+
+ // Apply filter preview
+ useEffect(() => {
+ if (!isPreviewMode || !activeFilter || !activeLayer?.canvas) {
+ return;
+ }
+
+ const canvas = activeLayer.canvas;
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return;
+
+ // Restore original
+ if (originalCanvasRef.current) {
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ ctx.drawImage(originalCanvasRef.current, 0, 0);
+ }
+
+ // Apply filter
+ try {
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+ const filteredData = applyFilter(imageData, activeFilter, params);
+ ctx.putImageData(filteredData, 0, 0);
+ } catch (error) {
+ console.error('Error applying filter preview:', error);
+ }
+ }, [isPreviewMode, activeFilter, params, activeLayer]);
+
+ // Cleanup on unmount
+ useEffect(() => {
+ return () => {
+ if (originalCanvasRef.current && activeLayer?.canvas) {
+ const ctx = activeLayer.canvas.getContext('2d');
+ if (ctx) {
+ ctx.clearRect(0, 0, activeLayer.canvas.width, activeLayer.canvas.height);
+ ctx.drawImage(originalCanvasRef.current, 0, 0);
+ }
+ }
+ };
+ }, []);
+
+ return {
+ isPreviewMode,
+ activeFilter,
+ };
+}
diff --git a/lib/filter-utils.ts b/lib/filter-utils.ts
new file mode 100644
index 0000000..9eafc88
--- /dev/null
+++ b/lib/filter-utils.ts
@@ -0,0 +1,429 @@
+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;
+ }
+}
diff --git a/store/filter-store.ts b/store/filter-store.ts
new file mode 100644
index 0000000..3d74f48
--- /dev/null
+++ b/store/filter-store.ts
@@ -0,0 +1,50 @@
+import { create } from 'zustand';
+import type { FilterType, FilterParams, FilterState } from '@/types/filter';
+
+interface FilterStore extends FilterState {
+ setActiveFilter: (filter: FilterType | null) => void;
+ updateParams: (params: Partial) => void;
+ resetParams: () => void;
+ setPreviewMode: (enabled: boolean) => void;
+}
+
+const defaultParams: FilterParams = {
+ brightness: 0,
+ contrast: 0,
+ hue: 0,
+ saturation: 0,
+ lightness: 0,
+ radius: 5,
+ amount: 50,
+ threshold: 128,
+ levels: 8,
+};
+
+export const useFilterStore = create((set) => ({
+ activeFilter: null,
+ params: { ...defaultParams },
+ isPreviewMode: false,
+ presets: [],
+
+ setActiveFilter: (filter) =>
+ set((state) => ({
+ activeFilter: filter,
+ params: filter ? { ...defaultParams } : state.params,
+ isPreviewMode: false,
+ })),
+
+ updateParams: (params) =>
+ set((state) => ({
+ params: { ...state.params, ...params },
+ })),
+
+ resetParams: () =>
+ set({
+ params: { ...defaultParams },
+ }),
+
+ setPreviewMode: (enabled) =>
+ set({
+ isPreviewMode: enabled,
+ }),
+}));
diff --git a/store/index.ts b/store/index.ts
index c4901af..1981e7b 100644
--- a/store/index.ts
+++ b/store/index.ts
@@ -1,5 +1,6 @@
export * from './canvas-store';
export * from './layer-store';
export * from './tool-store';
+export * from './filter-store';
export * from './history-store';
export * from './color-store';
diff --git a/types/filter.ts b/types/filter.ts
new file mode 100644
index 0000000..5f9f632
--- /dev/null
+++ b/types/filter.ts
@@ -0,0 +1,52 @@
+export type FilterType =
+ | 'brightness'
+ | 'contrast'
+ | 'hue-saturation'
+ | 'blur'
+ | 'sharpen'
+ | 'invert'
+ | 'grayscale'
+ | 'sepia'
+ | 'threshold'
+ | 'posterize';
+
+export interface FilterParams {
+ // Brightness/Contrast
+ brightness?: number; // -100 to 100
+ contrast?: number; // -100 to 100
+
+ // Hue/Saturation
+ hue?: number; // -180 to 180
+ saturation?: number; // -100 to 100
+ lightness?: number; // -100 to 100
+
+ // Blur
+ radius?: number; // 1 to 50
+
+ // Sharpen
+ amount?: number; // 0 to 100
+
+ // Threshold
+ threshold?: number; // 0 to 255
+
+ // Posterize
+ levels?: number; // 2 to 256
+}
+
+export interface Filter {
+ type: FilterType;
+ params: FilterParams;
+}
+
+export interface FilterPreset {
+ id: string;
+ name: string;
+ filter: Filter;
+}
+
+export interface FilterState {
+ activeFilter: FilterType | null;
+ params: FilterParams;
+ isPreviewMode: boolean;
+ presets: FilterPreset[];
+}
diff --git a/types/index.ts b/types/index.ts
index bf2e297..ef8f596 100644
--- a/types/index.ts
+++ b/types/index.ts
@@ -2,3 +2,4 @@ export * from './canvas';
export * from './layer';
export * from './tool';
export * from './history';
+export * from './filter';