|
|
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 { |
|
|
|