diff --git a/components/editor/editor-layout.tsx b/components/editor/editor-layout.tsx index b0822a5..632f844 100644 --- a/components/editor/editor-layout.tsx +++ b/components/editor/editor-layout.tsx @@ -5,6 +5,7 @@ import { useCanvasStore, useLayerStore } from '@/store'; import { useHistoryStore } from '@/store/history-store'; import { CanvasWithTools } from '@/components/canvas/canvas-with-tools'; import { FileMenu } from './file-menu'; +import { ImageMenu } from './image-menu'; import { ToolOptions } from './tool-options'; import { PanelDock } from './panel-dock'; import { ThemeToggle } from './theme-toggle'; @@ -101,12 +102,13 @@ export function EditorLayout() { {/* Header Bar */}
- {/* Left: Title and File Menu */} -
-

+ {/* Left: Title and Menus */} +
+

Paint UI

+
{/* Right: Controls */} diff --git a/components/editor/image-menu.tsx b/components/editor/image-menu.tsx new file mode 100644 index 0000000..2c323cd --- /dev/null +++ b/components/editor/image-menu.tsx @@ -0,0 +1,63 @@ +'use client'; + +import { useState } from 'react'; +import { AdjustmentsDialog } from '@/components/modals/adjustments-dialog'; +import { + Sliders, + ChevronDown, +} from 'lucide-react'; + +export function ImageMenu() { + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isAdjustmentsOpen, setIsAdjustmentsOpen] = useState(false); + + return ( + <> +
+ + + {isMenuOpen && ( + <> +
setIsMenuOpen(false)} + aria-hidden="true" + /> +
+ +
+ + )} +
+ + {/* Adjustments Dialog */} + setIsAdjustmentsOpen(false)} + /> + + ); +} diff --git a/components/modals/adjustments-dialog.tsx b/components/modals/adjustments-dialog.tsx new file mode 100644 index 0000000..d30d275 --- /dev/null +++ b/components/modals/adjustments-dialog.tsx @@ -0,0 +1,349 @@ +'use client'; + +import { useState, useEffect, useCallback } from 'react'; +import { useLayerStore } from '@/store'; +import { useHistoryStore } from '@/store/history-store'; +import { applyFilter } from '@/lib/filter-utils'; +import { FilterCommand } from '@/core/commands/filter-command'; +import type { FilterType, FilterParams } from '@/types/filter'; +import { X, RotateCcw } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +interface AdjustmentsDialogProps { + isOpen: boolean; + onClose: () => void; +} + +const FILTER_CATEGORIES = { + 'Color Adjustments': ['brightness', 'contrast', 'hue-saturation'] as FilterType[], + 'Filters': ['blur', 'sharpen'] as FilterType[], + 'Effects': ['invert', 'grayscale', 'sepia', 'threshold', 'posterize'] as FilterType[], +}; + +const FILTER_NAMES: Record = { + brightness: 'Brightness', + contrast: 'Contrast', + 'hue-saturation': 'Hue/Saturation', + blur: 'Blur', + sharpen: 'Sharpen', + invert: 'Invert Colors', + grayscale: 'Grayscale', + sepia: 'Sepia', + threshold: 'Threshold', + posterize: 'Posterize', +}; + +export function AdjustmentsDialog({ isOpen, onClose }: AdjustmentsDialogProps) { + const { getActiveLayer } = useLayerStore(); + const { executeCommand } = useHistoryStore(); + + const [selectedFilter, setSelectedFilter] = useState('brightness'); + const [params, setParams] = useState({ + brightness: 0, + contrast: 0, + hue: 0, + saturation: 0, + lightness: 0, + radius: 5, + amount: 50, + threshold: 128, + levels: 8, + }); + const [previewCanvas, setPreviewCanvas] = useState(null); + + const activeLayer = getActiveLayer(); + + // Update preview when params change + useEffect(() => { + if (!isOpen || !activeLayer?.canvas) return; + + const canvas = document.createElement('canvas'); + canvas.width = Math.min(activeLayer.canvas.width, 400); + canvas.height = Math.min(activeLayer.canvas.height, 400); + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + // Draw scaled layer + ctx.drawImage(activeLayer.canvas, 0, 0, canvas.width, canvas.height); + + // Apply filter to preview + const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); + const filtered = applyFilter(imageData, selectedFilter, params); + ctx.putImageData(filtered, 0, 0); + + setPreviewCanvas(canvas); + }, [isOpen, activeLayer, selectedFilter, params]); + + const handleParamChange = useCallback((key: keyof FilterParams, value: number) => { + setParams((prev) => ({ ...prev, [key]: value })); + }, []); + + const handleReset = useCallback(() => { + setParams({ + brightness: 0, + contrast: 0, + hue: 0, + saturation: 0, + lightness: 0, + radius: 5, + amount: 50, + threshold: 128, + levels: 8, + }); + }, []); + + const handleApply = useCallback(async () => { + if (!activeLayer) return; + + // Use the static method to apply filter and create command + const command = await FilterCommand.applyToLayerAsync(activeLayer, selectedFilter, params); + executeCommand(command); + + onClose(); + }, [activeLayer, selectedFilter, params, executeCommand, onClose]); + + if (!isOpen) return null; + + return ( + <> + {/* Backdrop */} +
+ + {/* Dialog */} +
+
+ {/* Header */} +
+

Filters & Adjustments

+ +
+ + {/* Content */} +
+ {/* Sidebar - Filter List */} +
+ {Object.entries(FILTER_CATEGORIES).map(([category, filters]) => ( +
+
+ {category} +
+
+ {filters.map((filter) => ( + + ))} +
+
+ ))} +
+ + {/* Main Area */} +
+ {/* Preview */} +
+ {previewCanvas && ( +
+ { + if (el && previewCanvas) { + el.width = previewCanvas.width; + el.height = previewCanvas.height; + const ctx = el.getContext('2d'); + if (ctx) ctx.drawImage(previewCanvas, 0, 0); + } + }} + className="border border-border rounded shadow-lg" + style={{ maxWidth: '400px', maxHeight: '400px' }} + /> +
+ Preview +
+
+ )} +
+ + {/* Controls */} +
+
+ {/* Brightness/Contrast */} + {selectedFilter === 'brightness' && ( +
+ + handleParamChange('brightness', Number(e.target.value))} + className="w-full" + /> +
+ )} + + {selectedFilter === 'contrast' && ( +
+ + handleParamChange('contrast', Number(e.target.value))} + className="w-full" + /> +
+ )} + + {selectedFilter === 'hue-saturation' && ( + <> +
+ + handleParamChange('hue', Number(e.target.value))} + className="w-full" + /> +
+
+ + handleParamChange('saturation', Number(e.target.value))} + className="w-full" + /> +
+
+ + handleParamChange('lightness', Number(e.target.value))} + className="w-full" + /> +
+ + )} + + {selectedFilter === 'blur' && ( +
+ + handleParamChange('radius', Number(e.target.value))} + className="w-full" + /> +
+ )} + + {selectedFilter === 'sharpen' && ( +
+ + handleParamChange('amount', Number(e.target.value))} + className="w-full" + /> +
+ )} + + {selectedFilter === 'threshold' && ( +
+ + handleParamChange('threshold', Number(e.target.value))} + className="w-full" + /> +
+ )} + + {selectedFilter === 'posterize' && ( +
+ + handleParamChange('levels', Number(e.target.value))} + className="w-full" + /> +
+ )} + + {/* One-click filters (no params) */} + {['invert', 'grayscale', 'sepia'].includes(selectedFilter) && ( +
+ This filter has no adjustable parameters. Click Apply to use it. +
+ )} +
+
+
+
+ + {/* Footer */} +
+ +
+ + +
+
+
+
+ + ); +}