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>
This commit is contained in:
79
hooks/use-filter-preview.ts
Normal file
79
hooks/use-filter-preview.ts
Normal file
@@ -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<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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user