MogensR's picture
Create web/src/lib/store/editor.ts
224165f
raw
history blame
7.5 kB
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
import { devtools } from 'zustand/middleware'
interface EditorState {
// Current state
image: File | null
processedImage: string | null
mask: string | null
background: string | null
isProcessing: boolean
zoom: number
tool: 'select' | 'brush' | 'eraser' | 'wand' | null
// History
history: Array<{
processedImage: string | null
mask: string | null
background: string | null
}>
historyIndex: number
// Settings
brushSize: number
brushHardness: number
tolerance: number
feather: number
edgeRefinement: number
// Actions
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
// History actions
saveToHistory: () => void
undo: () => void
redo: () => void
canUndo: boolean
canRedo: boolean
// Settings actions
setBrushSize: (size: number) => void
setBrushHardness: (hardness: number) => void
setTolerance: (tolerance: number) => void
setFeather: (feather: number) => void
setEdgeRefinement: (refinement: number) => void
// Utils
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 }
// Remove any history after current index
state.history = state.history.slice(0, state.historyIndex + 1)
// Add new entry
state.history.push(historyEntry)
state.historyIndex++
// Limit history to 50 entries
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')
// Create canvas and composite image with background
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
// Draw background if present
if (background) {
if (background.startsWith('#')) {
ctx.fillStyle = background
ctx.fillRect(0, 0, canvas.width, canvas.height)
} else if (background.startsWith('linear-gradient')) {
// Handle image backgrounds
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
}
}
// Draw the processed image
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 {
// Handle