Files
paint-ui/hooks/use-filter-preview.ts
Sebastian Krüger 924c10a3e4 feat(phase-7): implement comprehensive effects & filters system
This commit completes Phase 7 of the paint-ui implementation, adding a
complete filters and effects system with live preview capabilities.

**New Files:**
- types/filter.ts: Filter types, parameters, and state interfaces
- lib/filter-utils.ts: Core filter algorithms and image processing functions
- core/commands/filter-command.ts: Undo/redo support for filters
- store/filter-store.ts: Filter state management with Zustand
- hooks/use-filter-preview.ts: Real-time filter preview system
- components/filters/filter-panel.tsx: Complete filter UI with parameters
- components/filters/index.ts: Filters barrel export

**Updated Files:**
- components/editor/editor-layout.tsx: Integrated FilterPanel into layout
- store/index.ts: Added filter-store export
- types/index.ts: Added filter types export

**Implemented Filters:**

**Adjustment Filters (with parameters):**
-  Brightness (-100 to +100): Linear brightness adjustment
-  Contrast (-100 to +100): Contrast curve adjustment
-  Hue/Saturation/Lightness: Full HSL color manipulation
  - Hue: -180° to +180° rotation
  - Saturation: -100% to +100% adjustment
  - Lightness: -100% to +100% adjustment

**Effect Filters (with parameters):**
-  Gaussian Blur (1-50px): Separable kernel blur with proper edge handling
-  Sharpen (0-100%): Unsharp mask algorithm
-  Threshold (0-255): Binary threshold conversion
-  Posterize (2-256 levels): Color quantization

**One-Click Filters (no parameters):**
-  Invert: Color inversion
-  Grayscale: Luminosity-based desaturation
-  Sepia: Classic sepia tone effect

**Technical Features:**
- Real-time preview system with toggle control
- Non-destructive preview (restores original on cancel)
- Undo/redo integration via FilterCommand
- Efficient image processing with typed arrays
- HSL/RGB color space conversions
- Separable Gaussian blur for performance
- Proper clamping and edge case handling
- Layer-aware filtering (respects locked layers)

**UI/UX Features:**
- 264px wide filter panel with all filters listed
- Dynamic parameter controls based on selected filter
- Live preview toggle with visual feedback
- Apply/Cancel actions with proper state cleanup
- Disabled state when no unlocked layer selected
- Clear parameter labels and value display

**Algorithm Implementations:**
- Brightness: Linear RGB adjustment with clamping
- Contrast: Standard contrast curve (factor-based)
- Hue/Saturation: Full RGB↔HSL conversion with proper hue rotation
- Blur: Separable Gaussian kernel (horizontal + vertical passes)
- Sharpen: Convolution kernel with configurable amount
- Threshold: Luminosity-based binary conversion
- Posterize: Color quantization with configurable levels

Build verified: ✓ Compiled successfully in 1248ms

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-21 02:12:18 +01:00

80 lines
2.4 KiB
TypeScript

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<HTMLCanvasElement | null>(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,
};
}