| import { create } from 'zustand' |
| import { immer } from 'zustand/middleware/immer' |
| import { devtools } from 'zustand/middleware' |
|
|
| interface EditorState { |
| |
| image: File | null |
| processedImage: string | null |
| mask: string | null |
| background: string | null |
| isProcessing: boolean |
| zoom: number |
| tool: 'select' | 'brush' | 'eraser' | 'wand' | null |
| |
| |
| history: Array<{ |
| processedImage: string | null |
| mask: string | null |
| background: string | null |
| }> |
| historyIndex: number |
| |
| |
| brushSize: number |
| brushHardness: number |
| tolerance: number |
| feather: number |
| edgeRefinement: number |
| |
| |
| setImage: (image: File | null) => void |
| setProcessedImage: (image: string | null) => void |
| setMask: (mask: string | null) => void |
| setBackground: (background: string | null) => void |
| setIsProcessing: (processing: boolean) => void |
| setZoom: (zoom: number) => void |
| setTool: (tool: EditorState['tool']) => void |
| |
| |
| saveToHistory: () => void |
| undo: () => void |
| redo: () => void |
| canUndo: boolean |
| canRedo: boolean |
| |
| |
| setBrushSize: (size: number) => void |
| setBrushHardness: (hardness: number) => void |
| setTolerance: (tolerance: number) => void |
| setFeather: (feather: number) => void |
| setEdgeRefinement: (refinement: number) => void |
| |
| |
| reset: () => void |
| exportImage: (format: 'png' | 'jpeg' | 'webp', quality?: number) => Promise<Blob> |
| } |
|
|
| const initialState = { |
| image: null, |
| processedImage: null, |
| mask: null, |
| background: null, |
| isProcessing: false, |
| zoom: 100, |
| tool: null, |
| history: [], |
| historyIndex: -1, |
| brushSize: 20, |
| brushHardness: 80, |
| tolerance: 20, |
| feather: 2, |
| edgeRefinement: 50, |
| } |
|
|
| export const useEditorStore = create<EditorState>()( |
| devtools( |
| immer((set, get) => ({ |
| ...initialState, |
| |
| setImage: (image) => |
| set((state) => { |
| state.image = image |
| if (!image) { |
| state.processedImage = null |
| state.mask = null |
| state.background = null |
| state.history = [] |
| state.historyIndex = -1 |
| } |
| }), |
| |
| setProcessedImage: (image) => |
| set((state) => { |
| state.processedImage = image |
| }), |
| |
| setMask: (mask) => |
| set((state) => { |
| state.mask = mask |
| }), |
| |
| setBackground: (background) => |
| set((state) => { |
| state.background = background |
| get().saveToHistory() |
| }), |
| |
| setIsProcessing: (processing) => |
| set((state) => { |
| state.isProcessing = processing |
| }), |
| |
| setZoom: (zoom) => |
| set((state) => { |
| state.zoom = Math.max(10, Math.min(500, zoom)) |
| }), |
| |
| setTool: (tool) => |
| set((state) => { |
| state.tool = tool |
| }), |
| |
| saveToHistory: () => |
| set((state) => { |
| const { processedImage, mask, background } = state |
| const historyEntry = { processedImage, mask, background } |
| |
| |
| state.history = state.history.slice(0, state.historyIndex + 1) |
| |
| |
| state.history.push(historyEntry) |
| state.historyIndex++ |
| |
| |
| if (state.history.length > 50) { |
| state.history.shift() |
| state.historyIndex-- |
| } |
| }), |
| |
| undo: () => |
| set((state) => { |
| if (state.historyIndex > 0) { |
| state.historyIndex-- |
| const entry = state.history[state.historyIndex] |
| state.processedImage = entry.processedImage |
| state.mask = entry.mask |
| state.background = entry.background |
| } |
| }), |
| |
| redo: () => |
| set((state) => { |
| if (state.historyIndex < state.history.length - 1) { |
| state.historyIndex++ |
| const entry = state.history[state.historyIndex] |
| state.processedImage = entry.processedImage |
| state.mask = entry.mask |
| state.background = entry.background |
| } |
| }), |
| |
| get canUndo() { |
| return get().historyIndex > 0 |
| }, |
| |
| get canRedo() { |
| return get().historyIndex < get().history.length - 1 |
| }, |
| |
| setBrushSize: (size) => |
| set((state) => { |
| state.brushSize = size |
| }), |
| |
| setBrushHardness: (hardness) => |
| set((state) => { |
| state.brushHardness = hardness |
| }), |
| |
| setTolerance: (tolerance) => |
| set((state) => { |
| state.tolerance = tolerance |
| }), |
| |
| setFeather: (feather) => |
| set((state) => { |
| state.feather = feather |
| }), |
| |
| setEdgeRefinement: (refinement) => |
| set((state) => { |
| state.edgeRefinement = refinement |
| }), |
| |
| reset: () => |
| set(() => initialState), |
| |
| exportImage: async (format, quality = 0.95) => { |
| const { processedImage, background } = get() |
| if (!processedImage) throw new Error('No image to export') |
| |
| |
| const canvas = document.createElement('canvas') |
| const ctx = canvas.getContext('2d') |
| if (!ctx) throw new Error('Failed to create canvas context') |
| |
| return new Promise<Blob>((resolve, reject) => { |
| const img = new Image() |
| img.onload = () => { |
| canvas.width = img.width |
| canvas.height = img.height |
| |
| |
| if (background) { |
| if (background.startsWith('#')) { |
| ctx.fillStyle = background |
| ctx.fillRect(0, 0, canvas.width, canvas.height) |
| } else if (background.startsWith('linear-gradient')) { |
| |
| const bgImg = new Image() |
| bgImg.onload = () => { |
| ctx.drawImage(bgImg, 0, 0, canvas.width, canvas.height) |
| ctx.drawImage(img, 0, 0) |
| canvas.toBlob( |
| (blob) => { |
| if (blob) resolve(blob) |
| else reject(new Error('Failed to export image')) |
| }, |
| `image/${format}`, |
| quality |
| ) |
| } |
| bgImg.src = background |
| return |
| } |
| } |
| |
| |
| ctx.drawImage(img, 0, 0) |
| |
| canvas.toBlob( |
| (blob) => { |
| if (blob) resolve(blob) |
| else reject(new Error('Failed to export image')) |
| }, |
| `image/${format}`, |
| quality |
| ) |
| } |
| img.onerror = () => reject(new Error('Failed to load image')) |
| img.src = processedImage |
| }) |
| }, |
| })) |
| ) |
| ) gradient backgrounds |
| const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height) |
| gradient.addColorStop(0, '#667eea') |
| gradient.addColorStop(1, '#764ba2') |
| ctx.fillStyle = gradient |
| ctx.fillRect(0, 0, canvas.width, canvas.height) |
| } else { |
| |