Files
paint-ui/hooks/use-filter-preview.ts

80 lines
2.4 KiB
TypeScript
Raw Permalink Normal View History

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
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,
};
}