MogensR commited on
Commit
224165f
·
1 Parent(s): 3a2dfa1

Create web/src/lib/store/editor.ts

Browse files
Files changed (1) hide show
  1. web/src/lib/store/editor.ts +265 -0
web/src/lib/store/editor.ts ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { create } from 'zustand'
2
+ import { immer } from 'zustand/middleware/immer'
3
+ import { devtools } from 'zustand/middleware'
4
+
5
+ interface EditorState {
6
+ // Current state
7
+ image: File | null
8
+ processedImage: string | null
9
+ mask: string | null
10
+ background: string | null
11
+ isProcessing: boolean
12
+ zoom: number
13
+ tool: 'select' | 'brush' | 'eraser' | 'wand' | null
14
+
15
+ // History
16
+ history: Array<{
17
+ processedImage: string | null
18
+ mask: string | null
19
+ background: string | null
20
+ }>
21
+ historyIndex: number
22
+
23
+ // Settings
24
+ brushSize: number
25
+ brushHardness: number
26
+ tolerance: number
27
+ feather: number
28
+ edgeRefinement: number
29
+
30
+ // Actions
31
+ setImage: (image: File | null) => void
32
+ setProcessedImage: (image: string | null) => void
33
+ setMask: (mask: string | null) => void
34
+ setBackground: (background: string | null) => void
35
+ setIsProcessing: (processing: boolean) => void
36
+ setZoom: (zoom: number) => void
37
+ setTool: (tool: EditorState['tool']) => void
38
+
39
+ // History actions
40
+ saveToHistory: () => void
41
+ undo: () => void
42
+ redo: () => void
43
+ canUndo: boolean
44
+ canRedo: boolean
45
+
46
+ // Settings actions
47
+ setBrushSize: (size: number) => void
48
+ setBrushHardness: (hardness: number) => void
49
+ setTolerance: (tolerance: number) => void
50
+ setFeather: (feather: number) => void
51
+ setEdgeRefinement: (refinement: number) => void
52
+
53
+ // Utils
54
+ reset: () => void
55
+ exportImage: (format: 'png' | 'jpeg' | 'webp', quality?: number) => Promise<Blob>
56
+ }
57
+
58
+ const initialState = {
59
+ image: null,
60
+ processedImage: null,
61
+ mask: null,
62
+ background: null,
63
+ isProcessing: false,
64
+ zoom: 100,
65
+ tool: null,
66
+ history: [],
67
+ historyIndex: -1,
68
+ brushSize: 20,
69
+ brushHardness: 80,
70
+ tolerance: 20,
71
+ feather: 2,
72
+ edgeRefinement: 50,
73
+ }
74
+
75
+ export const useEditorStore = create<EditorState>()(
76
+ devtools(
77
+ immer((set, get) => ({
78
+ ...initialState,
79
+
80
+ setImage: (image) =>
81
+ set((state) => {
82
+ state.image = image
83
+ if (!image) {
84
+ state.processedImage = null
85
+ state.mask = null
86
+ state.background = null
87
+ state.history = []
88
+ state.historyIndex = -1
89
+ }
90
+ }),
91
+
92
+ setProcessedImage: (image) =>
93
+ set((state) => {
94
+ state.processedImage = image
95
+ }),
96
+
97
+ setMask: (mask) =>
98
+ set((state) => {
99
+ state.mask = mask
100
+ }),
101
+
102
+ setBackground: (background) =>
103
+ set((state) => {
104
+ state.background = background
105
+ get().saveToHistory()
106
+ }),
107
+
108
+ setIsProcessing: (processing) =>
109
+ set((state) => {
110
+ state.isProcessing = processing
111
+ }),
112
+
113
+ setZoom: (zoom) =>
114
+ set((state) => {
115
+ state.zoom = Math.max(10, Math.min(500, zoom))
116
+ }),
117
+
118
+ setTool: (tool) =>
119
+ set((state) => {
120
+ state.tool = tool
121
+ }),
122
+
123
+ saveToHistory: () =>
124
+ set((state) => {
125
+ const { processedImage, mask, background } = state
126
+ const historyEntry = { processedImage, mask, background }
127
+
128
+ // Remove any history after current index
129
+ state.history = state.history.slice(0, state.historyIndex + 1)
130
+
131
+ // Add new entry
132
+ state.history.push(historyEntry)
133
+ state.historyIndex++
134
+
135
+ // Limit history to 50 entries
136
+ if (state.history.length > 50) {
137
+ state.history.shift()
138
+ state.historyIndex--
139
+ }
140
+ }),
141
+
142
+ undo: () =>
143
+ set((state) => {
144
+ if (state.historyIndex > 0) {
145
+ state.historyIndex--
146
+ const entry = state.history[state.historyIndex]
147
+ state.processedImage = entry.processedImage
148
+ state.mask = entry.mask
149
+ state.background = entry.background
150
+ }
151
+ }),
152
+
153
+ redo: () =>
154
+ set((state) => {
155
+ if (state.historyIndex < state.history.length - 1) {
156
+ state.historyIndex++
157
+ const entry = state.history[state.historyIndex]
158
+ state.processedImage = entry.processedImage
159
+ state.mask = entry.mask
160
+ state.background = entry.background
161
+ }
162
+ }),
163
+
164
+ get canUndo() {
165
+ return get().historyIndex > 0
166
+ },
167
+
168
+ get canRedo() {
169
+ return get().historyIndex < get().history.length - 1
170
+ },
171
+
172
+ setBrushSize: (size) =>
173
+ set((state) => {
174
+ state.brushSize = size
175
+ }),
176
+
177
+ setBrushHardness: (hardness) =>
178
+ set((state) => {
179
+ state.brushHardness = hardness
180
+ }),
181
+
182
+ setTolerance: (tolerance) =>
183
+ set((state) => {
184
+ state.tolerance = tolerance
185
+ }),
186
+
187
+ setFeather: (feather) =>
188
+ set((state) => {
189
+ state.feather = feather
190
+ }),
191
+
192
+ setEdgeRefinement: (refinement) =>
193
+ set((state) => {
194
+ state.edgeRefinement = refinement
195
+ }),
196
+
197
+ reset: () =>
198
+ set(() => initialState),
199
+
200
+ exportImage: async (format, quality = 0.95) => {
201
+ const { processedImage, background } = get()
202
+ if (!processedImage) throw new Error('No image to export')
203
+
204
+ // Create canvas and composite image with background
205
+ const canvas = document.createElement('canvas')
206
+ const ctx = canvas.getContext('2d')
207
+ if (!ctx) throw new Error('Failed to create canvas context')
208
+
209
+ return new Promise<Blob>((resolve, reject) => {
210
+ const img = new Image()
211
+ img.onload = () => {
212
+ canvas.width = img.width
213
+ canvas.height = img.height
214
+
215
+ // Draw background if present
216
+ if (background) {
217
+ if (background.startsWith('#')) {
218
+ ctx.fillStyle = background
219
+ ctx.fillRect(0, 0, canvas.width, canvas.height)
220
+ } else if (background.startsWith('linear-gradient')) {
221
+ // Handle image backgrounds
222
+ const bgImg = new Image()
223
+ bgImg.onload = () => {
224
+ ctx.drawImage(bgImg, 0, 0, canvas.width, canvas.height)
225
+ ctx.drawImage(img, 0, 0)
226
+ canvas.toBlob(
227
+ (blob) => {
228
+ if (blob) resolve(blob)
229
+ else reject(new Error('Failed to export image'))
230
+ },
231
+ `image/${format}`,
232
+ quality
233
+ )
234
+ }
235
+ bgImg.src = background
236
+ return
237
+ }
238
+ }
239
+
240
+ // Draw the processed image
241
+ ctx.drawImage(img, 0, 0)
242
+
243
+ canvas.toBlob(
244
+ (blob) => {
245
+ if (blob) resolve(blob)
246
+ else reject(new Error('Failed to export image'))
247
+ },
248
+ `image/${format}`,
249
+ quality
250
+ )
251
+ }
252
+ img.onerror = () => reject(new Error('Failed to load image'))
253
+ img.src = processedImage
254
+ })
255
+ },
256
+ }))
257
+ )
258
+ ) gradient backgrounds
259
+ const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height)
260
+ gradient.addColorStop(0, '#667eea')
261
+ gradient.addColorStop(1, '#764ba2')
262
+ ctx.fillStyle = gradient
263
+ ctx.fillRect(0, 0, canvas.width, canvas.height)
264
+ } else {
265
+ // Handle