@gradio/imageeditor 0.18.2-dev.0 → 0.18.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/Index.svelte +122 -159
- package/InteractiveImageEditor.svelte +1 -0
- package/dist/Example.svelte +7 -4
- package/dist/Example.svelte.d.ts +20 -18
- package/dist/Index.svelte +152 -136
- package/dist/Index.svelte.d.ts +18 -202
- package/dist/InteractiveImageEditor.svelte +205 -148
- package/dist/InteractiveImageEditor.svelte.d.ts +55 -51
- package/dist/shared/Anchor.svelte +210 -136
- package/dist/shared/Anchor.svelte.d.ts +18 -16
- package/dist/shared/Controls.svelte +55 -36
- package/dist/shared/Controls.svelte.d.ts +35 -33
- package/dist/shared/IconButton.svelte +19 -17
- package/dist/shared/IconButton.svelte.d.ts +34 -32
- package/dist/shared/ImageEditor.svelte +673 -466
- package/dist/shared/ImageEditor.svelte.d.ts +4 -79
- package/dist/shared/Layers.svelte +52 -30
- package/dist/shared/Layers.svelte.d.ts +39 -36
- package/dist/shared/Resize.svelte +42 -21
- package/dist/shared/Resize.svelte.d.ts +29 -27
- package/dist/shared/SecondaryToolbar.svelte +17 -5
- package/dist/shared/SecondaryToolbar.svelte.d.ts +38 -36
- package/dist/shared/Toolbar.svelte +123 -69
- package/dist/shared/Toolbar.svelte.d.ts +40 -38
- package/dist/shared/brush/BrushOptions.svelte +119 -84
- package/dist/shared/brush/BrushOptions.svelte.d.ts +29 -27
- package/dist/shared/brush/BrushSize.svelte +12 -7
- package/dist/shared/brush/BrushSize.svelte.d.ts +23 -20
- package/dist/shared/brush/ColorField.svelte +49 -33
- package/dist/shared/brush/ColorField.svelte.d.ts +23 -21
- package/dist/shared/brush/ColorPicker.svelte +152 -108
- package/dist/shared/brush/ColorPicker.svelte.d.ts +19 -17
- package/dist/shared/brush/ColorSwatch.svelte +78 -46
- package/dist/shared/brush/ColorSwatch.svelte.d.ts +34 -32
- package/dist/types.d.ts +40 -0
- package/dist/types.js +25 -0
- package/package.json +12 -12
- package/shared/ImageEditor.svelte +1 -3
- package/shared/brush/BrushOptions.svelte +1 -1
- package/shared/brush/ColorPicker.svelte +2 -1
- package/types.ts +74 -0
|
@@ -1,471 +1,678 @@
|
|
|
1
|
-
<script
|
|
1
|
+
<script lang="ts" context="module">
|
|
2
|
+
import type { Tool, Subtool } from "./Toolbar.svelte";
|
|
3
|
+
|
|
4
|
+
export const EDITOR_KEY = Symbol("editor");
|
|
5
|
+
export type context_type = "bg" | "layers" | "crop" | "draw" | "erase";
|
|
2
6
|
</script>
|
|
3
7
|
|
|
4
|
-
<script
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import
|
|
11
|
-
import {
|
|
12
|
-
import {} from "
|
|
13
|
-
import {
|
|
14
|
-
import {} from "./
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import
|
|
18
|
-
import {
|
|
19
|
-
import {} from "
|
|
20
|
-
import
|
|
21
|
-
import
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
export
|
|
39
|
-
|
|
40
|
-
export let
|
|
41
|
-
export let
|
|
42
|
-
export let
|
|
43
|
-
export let
|
|
44
|
-
export let
|
|
45
|
-
export let
|
|
46
|
-
export let
|
|
47
|
-
let
|
|
48
|
-
let
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
let
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
let
|
|
211
|
-
let
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
)
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
function
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
import { onMount, createEventDispatcher, tick } from "svelte";
|
|
10
|
+
import { get } from "svelte/store";
|
|
11
|
+
import Toolbar, { type Tool as ToolbarTool } from "./Toolbar.svelte";
|
|
12
|
+
import { CropTool } from "./crop/crop";
|
|
13
|
+
import { ResizeTool } from "./resize/resize";
|
|
14
|
+
import { Webcam } from "@gradio/image";
|
|
15
|
+
import type { I18nFormatter } from "@gradio/utils";
|
|
16
|
+
import type { Client, FileData } from "@gradio/client";
|
|
17
|
+
import tinycolor, { type ColorInput } from "tinycolor2";
|
|
18
|
+
import { ZoomTool } from "./zoom/zoom";
|
|
19
|
+
import { type CommandManager, type CommandNode } from "./core/commands";
|
|
20
|
+
import { ImageEditor } from "./core/editor";
|
|
21
|
+
import { type Brush, type Eraser } from "./brush/types";
|
|
22
|
+
import { BrushTool } from "./brush/brush";
|
|
23
|
+
import { create_drag } from "@gradio/upload";
|
|
24
|
+
import SecondaryToolbar from "./SecondaryToolbar.svelte";
|
|
25
|
+
import { Check } from "@gradio/icons";
|
|
26
|
+
import type { LayerOptions, Source, Transform, WebcamOptions } from "./types";
|
|
27
|
+
|
|
28
|
+
import { type ImageBlobs } from "./types";
|
|
29
|
+
import Controls from "./Controls.svelte";
|
|
30
|
+
import IconButton from "./IconButton.svelte";
|
|
31
|
+
|
|
32
|
+
const { drag, open_file_upload } = create_drag();
|
|
33
|
+
const dispatch = createEventDispatcher<{
|
|
34
|
+
clear?: never;
|
|
35
|
+
save: void;
|
|
36
|
+
change: void;
|
|
37
|
+
upload: void;
|
|
38
|
+
input: void;
|
|
39
|
+
download_error: string;
|
|
40
|
+
}>();
|
|
41
|
+
|
|
42
|
+
export const antialias = true;
|
|
43
|
+
|
|
44
|
+
export let changeable = false;
|
|
45
|
+
export let sources: Source[] = ["upload", "webcam", "clipboard"];
|
|
46
|
+
export let transforms: Transform[] = ["crop", "resize"];
|
|
47
|
+
export let canvas_size: [number, number];
|
|
48
|
+
export let is_dragging = false;
|
|
49
|
+
export let background_image = false;
|
|
50
|
+
export let brush_options: Brush;
|
|
51
|
+
export let eraser_options: Eraser;
|
|
52
|
+
export let fixed_canvas = false;
|
|
53
|
+
export let root: string;
|
|
54
|
+
export let i18n: I18nFormatter;
|
|
55
|
+
export let upload: Client["upload"];
|
|
56
|
+
export let composite: FileData | null;
|
|
57
|
+
export let layers: FileData[];
|
|
58
|
+
export let background: FileData | null;
|
|
59
|
+
export let border_region = 0;
|
|
60
|
+
export let layer_options: LayerOptions;
|
|
61
|
+
export let current_tool: ToolbarTool;
|
|
62
|
+
export let webcam_options: WebcamOptions;
|
|
63
|
+
export let show_download_button = false;
|
|
64
|
+
export let theme_mode: "dark" | "light";
|
|
65
|
+
export let full_history: CommandNode | null = null;
|
|
66
|
+
|
|
67
|
+
let pixi_target: HTMLDivElement;
|
|
68
|
+
let pixi_target_crop: HTMLDivElement;
|
|
69
|
+
|
|
70
|
+
$: if (layer_options) {
|
|
71
|
+
if (check_if_should_init()) {
|
|
72
|
+
editor.set_layer_options(layer_options);
|
|
73
|
+
refresh_tools();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function refresh_tools(): void {
|
|
78
|
+
if (!editor || !ready) return;
|
|
79
|
+
editor.set_tool(current_tool);
|
|
80
|
+
editor.set_subtool(current_subtool);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
$: if (editor && ready && editor.layers) {
|
|
84
|
+
const current_layers = get(editor.layers);
|
|
85
|
+
|
|
86
|
+
if (current_layers.layers.length > 0 && !current_layers.active_layer) {
|
|
87
|
+
refresh_tools_for_layer_changes(current_layers);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Refreshes tools when layer state changes
|
|
93
|
+
*/
|
|
94
|
+
function refresh_tools_for_layer_changes(current_layers: any): void {
|
|
95
|
+
if (!editor || !ready) return;
|
|
96
|
+
|
|
97
|
+
if (current_layers.layers.length > 0 && !current_layers.active_layer) {
|
|
98
|
+
editor.set_layer(current_layers.layers[0].id);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// reapply current tool to ensure it targets the correct layer
|
|
102
|
+
if (current_tool) {
|
|
103
|
+
editor.set_tool(current_tool);
|
|
104
|
+
if (current_subtool) {
|
|
105
|
+
editor.set_subtool(current_subtool);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (brush && (current_tool === "draw" || current_tool === "erase")) {
|
|
110
|
+
brush.set_tool(current_tool, current_subtool);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function check_if_should_init(): boolean {
|
|
115
|
+
return layer_options && editor && ready;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export let has_drawn = false;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Gets the image blobs from the editor
|
|
122
|
+
* @returns {Promise<ImageBlobs>} Object containing background, layers, and composite image blobs
|
|
123
|
+
*/
|
|
124
|
+
export async function get_blobs(): Promise<ImageBlobs> {
|
|
125
|
+
if (!editor) return { background: null, layers: [], composite: null };
|
|
126
|
+
if (!background_image && !has_drawn && !layers.length)
|
|
127
|
+
return { background: null, layers: [], composite: null };
|
|
128
|
+
const blobs = await editor.get_blobs();
|
|
129
|
+
return blobs;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let editor: ImageEditor;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Adds an image to the editor
|
|
136
|
+
* @param {Blob | File} image - The image to add
|
|
137
|
+
*/
|
|
138
|
+
export function add_image(image: Blob | File): void {
|
|
139
|
+
editor.add_image({ image });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let pending_bg: Promise<void>;
|
|
143
|
+
/**
|
|
144
|
+
* Adds an image to the editor from a URL
|
|
145
|
+
* @param {string | FileData} source - The URL of the image or a FileData object
|
|
146
|
+
* @returns {Promise<void>}
|
|
147
|
+
*/
|
|
148
|
+
export async function add_image_from_url(
|
|
149
|
+
source:
|
|
150
|
+
| string
|
|
151
|
+
| {
|
|
152
|
+
url: string;
|
|
153
|
+
meta: {
|
|
154
|
+
_type: string;
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
| any
|
|
158
|
+
): Promise<void> {
|
|
159
|
+
if (!editor || !source || !check_if_should_init()) return;
|
|
160
|
+
let url: string;
|
|
161
|
+
|
|
162
|
+
if (typeof source === "string") {
|
|
163
|
+
url = source;
|
|
164
|
+
} else if (source?.meta?._type === "gradio.FileData" && source?.url) {
|
|
165
|
+
url = source.url;
|
|
166
|
+
} else {
|
|
167
|
+
console.warn("Invalid source provided to add_image_from_url:", source);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
pending_bg = editor.add_image_from_url(url);
|
|
173
|
+
let pending_crop = crop.add_image_from_url(url);
|
|
174
|
+
await Promise.all([pending_bg, pending_crop]);
|
|
175
|
+
crop.set_tool("image");
|
|
176
|
+
crop.set_subtool("crop");
|
|
177
|
+
background_image = true;
|
|
178
|
+
zoom.set_zoom("fit");
|
|
179
|
+
dispatch("upload");
|
|
180
|
+
dispatch("input");
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error("Error adding image from URL:", error);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Adds a new layer with an image loaded from a URL
|
|
188
|
+
* @param {string | FileData} source - The URL of the image or a FileData object
|
|
189
|
+
* @returns {Promise<string | null>} - The ID of the created layer, or null if failed
|
|
190
|
+
*/
|
|
191
|
+
export async function add_layers_from_url(
|
|
192
|
+
source: FileData[] | any
|
|
193
|
+
): Promise<void> {
|
|
194
|
+
if (!editor || !source.length || !check_if_should_init()) return;
|
|
195
|
+
|
|
196
|
+
if (
|
|
197
|
+
Array.isArray(source) &&
|
|
198
|
+
source.every((item) => item?.meta?._type === "gradio.FileData")
|
|
199
|
+
) {
|
|
200
|
+
try {
|
|
201
|
+
await pending_bg;
|
|
202
|
+
await editor.add_layers_from_url(source.map((item) => item.url));
|
|
203
|
+
dispatch("change");
|
|
204
|
+
dispatch("input");
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error("Error adding layer from URL:", error);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let brush: BrushTool;
|
|
212
|
+
let zoom: ZoomTool;
|
|
213
|
+
let zoom_level = 1;
|
|
214
|
+
let ready = false;
|
|
215
|
+
let mounted = false;
|
|
216
|
+
let min_zoom = true;
|
|
217
|
+
|
|
218
|
+
let last_dimensions = { width: 0, height: 0 };
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Handles visibility changes and resets zoom if dimensions have changed
|
|
222
|
+
*/
|
|
223
|
+
async function handle_visibility_change(): Promise<void> {
|
|
224
|
+
if (!editor || !ready || !zoom) return;
|
|
225
|
+
await tick();
|
|
226
|
+
|
|
227
|
+
const is_visible = pixi_target.offsetParent !== null;
|
|
228
|
+
|
|
229
|
+
if (is_visible) {
|
|
230
|
+
const current_dimensions = pixi_target.getBoundingClientRect();
|
|
231
|
+
|
|
232
|
+
if (
|
|
233
|
+
current_dimensions.width !== last_dimensions.width ||
|
|
234
|
+
current_dimensions.height !== last_dimensions.height
|
|
235
|
+
) {
|
|
236
|
+
zoom.set_zoom("fit");
|
|
237
|
+
|
|
238
|
+
last_dimensions = {
|
|
239
|
+
width: current_dimensions.width,
|
|
240
|
+
height: current_dimensions.height
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
onMount(() => {
|
|
247
|
+
let intersection_observer: IntersectionObserver;
|
|
248
|
+
let resize_observer: ResizeObserver;
|
|
249
|
+
|
|
250
|
+
init_image_editor().then(() => {
|
|
251
|
+
mounted = true;
|
|
252
|
+
intersection_observer = new IntersectionObserver(() => {
|
|
253
|
+
handle_visibility_change();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
resize_observer = new ResizeObserver(() => {
|
|
257
|
+
handle_visibility_change();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
intersection_observer.observe(pixi_target);
|
|
261
|
+
resize_observer.observe(pixi_target);
|
|
262
|
+
|
|
263
|
+
setTimeout(() => {
|
|
264
|
+
if (full_history && editor) {
|
|
265
|
+
editor.command_manager
|
|
266
|
+
.replay(full_history, editor.context)
|
|
267
|
+
.then(() => {
|
|
268
|
+
refresh_tools_after_history();
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}, 0);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
if (typeof window !== "undefined") {
|
|
275
|
+
(window as any).editor = editor;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return () => {
|
|
279
|
+
if (intersection_observer) {
|
|
280
|
+
intersection_observer.disconnect();
|
|
281
|
+
}
|
|
282
|
+
if (resize_observer) {
|
|
283
|
+
resize_observer.disconnect();
|
|
284
|
+
}
|
|
285
|
+
if (editor) {
|
|
286
|
+
editor.destroy();
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Refreshes tool state after history replay to ensure proper layer targeting
|
|
293
|
+
*/
|
|
294
|
+
function refresh_tools_after_history(): void {
|
|
295
|
+
if (!editor || !ready) return;
|
|
296
|
+
|
|
297
|
+
const current_layers = get(editor.layers);
|
|
298
|
+
|
|
299
|
+
if (current_layers.layers.length > 0 && !current_layers.active_layer) {
|
|
300
|
+
editor.set_layer(current_layers.layers[0].id);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (current_tool) {
|
|
304
|
+
editor.set_tool(current_tool);
|
|
305
|
+
if (current_subtool) {
|
|
306
|
+
editor.set_subtool(current_subtool);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (brush && (current_tool === "draw" || current_tool === "erase")) {
|
|
311
|
+
brush.set_tool(current_tool, current_subtool);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
full_history = editor.command_manager.history;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
let crop: ImageEditor;
|
|
318
|
+
let crop_zoom: ZoomTool;
|
|
319
|
+
export let can_undo = false;
|
|
320
|
+
let can_redo = false;
|
|
321
|
+
async function init_image_editor(): Promise<void> {
|
|
322
|
+
brush = new BrushTool();
|
|
323
|
+
zoom = new ZoomTool();
|
|
324
|
+
editor = new ImageEditor({
|
|
325
|
+
target_element: pixi_target,
|
|
326
|
+
width: canvas_size[0],
|
|
327
|
+
height: canvas_size[1],
|
|
328
|
+
tools: ["image", zoom, new ResizeTool(), brush],
|
|
329
|
+
fixed_canvas,
|
|
330
|
+
border_region,
|
|
331
|
+
layer_options,
|
|
332
|
+
theme_mode
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
brush.on("change", () => {
|
|
336
|
+
has_drawn = true;
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
crop_zoom = new ZoomTool();
|
|
340
|
+
|
|
341
|
+
crop = new ImageEditor({
|
|
342
|
+
target_element: pixi_target_crop,
|
|
343
|
+
width: canvas_size[0],
|
|
344
|
+
height: canvas_size[1],
|
|
345
|
+
tools: ["image", crop_zoom, new CropTool()],
|
|
346
|
+
dark: true,
|
|
347
|
+
fixed_canvas: false,
|
|
348
|
+
border_region: 0,
|
|
349
|
+
pad_bottom: 40
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
editor.scale.subscribe((_scale) => {
|
|
353
|
+
zoom_level = _scale;
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
editor.min_zoom.subscribe((is_min_zoom) => {
|
|
357
|
+
min_zoom = is_min_zoom;
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
editor.dimensions.subscribe((dimensions) => {
|
|
361
|
+
last_dimensions = { ...dimensions };
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
editor.command_manager.current_history.subscribe((history) => {
|
|
365
|
+
can_undo = history.previous !== null;
|
|
366
|
+
can_redo = history.next !== null;
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
await Promise.all([editor.ready, crop.ready]).then(() => {
|
|
370
|
+
handle_tool_change({ tool: "image" });
|
|
371
|
+
ready = true;
|
|
372
|
+
if (sources.length > 0) {
|
|
373
|
+
handle_tool_change({ tool: "image" });
|
|
374
|
+
} else {
|
|
375
|
+
handle_tool_change({ tool: "draw" });
|
|
376
|
+
}
|
|
377
|
+
crop.set_subtool("crop");
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
editor.on("change", () => {
|
|
381
|
+
dispatch("change");
|
|
382
|
+
dispatch("input");
|
|
383
|
+
full_history = editor.command_manager.history;
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
if (background || layers.length > 0) {
|
|
387
|
+
if (background) {
|
|
388
|
+
await add_image_from_url(background);
|
|
389
|
+
}
|
|
390
|
+
if (layers.length > 0) {
|
|
391
|
+
await add_layers_from_url(layers);
|
|
392
|
+
}
|
|
393
|
+
handle_tool_change({ tool: "draw" });
|
|
394
|
+
} else if (composite) {
|
|
395
|
+
await add_image_from_url(composite);
|
|
396
|
+
handle_tool_change({ tool: "draw" });
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
refresh_tools();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
$: if (
|
|
403
|
+
background == null &&
|
|
404
|
+
layers.length == 0 &&
|
|
405
|
+
composite == null &&
|
|
406
|
+
editor &&
|
|
407
|
+
ready
|
|
408
|
+
) {
|
|
409
|
+
editor.reset_canvas();
|
|
410
|
+
zoom.set_zoom("fit");
|
|
411
|
+
handle_tool_change({ tool: "image" });
|
|
412
|
+
background_image = false;
|
|
413
|
+
has_drawn = false;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
$: current_tool === "image" &&
|
|
417
|
+
current_subtool === "crop" &&
|
|
418
|
+
crop_zoom.set_zoom("fit");
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Handles file uploads
|
|
422
|
+
* @param {File[]} files - The uploaded files
|
|
423
|
+
*/
|
|
424
|
+
async function handle_files(
|
|
425
|
+
files: File[] | Blob[] | File | Blob | null
|
|
426
|
+
): Promise<void> {
|
|
427
|
+
if (files == null) return;
|
|
428
|
+
if (!sources.includes("upload")) return;
|
|
429
|
+
editor.reset_canvas();
|
|
430
|
+
const _file = Array.isArray(files) ? files[0] : files;
|
|
431
|
+
await editor.add_image({ image: _file });
|
|
432
|
+
await crop.add_image({ image: _file });
|
|
433
|
+
crop.reset();
|
|
434
|
+
|
|
435
|
+
background_image = true;
|
|
436
|
+
zoom.set_zoom("fit");
|
|
437
|
+
handle_tool_change({ tool: "draw" });
|
|
438
|
+
dispatch("upload");
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
$: background_image = can_undo && editor.command_manager.contains("AddImage");
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Handles tool change events
|
|
445
|
+
* @param {{ tool: ToolbarTool }} param0 - Object containing the selected tool
|
|
446
|
+
*/
|
|
447
|
+
function handle_tool_change({ tool }: { tool: ToolbarTool }): void {
|
|
448
|
+
editor.set_tool(tool);
|
|
449
|
+
current_tool = tool;
|
|
450
|
+
|
|
451
|
+
if (tool === "image") {
|
|
452
|
+
crop.set_tool("image");
|
|
453
|
+
crop.set_subtool("crop");
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Handles subtool change events
|
|
459
|
+
* @param {{ tool: ToolbarTool, subtool: Subtool }} param0 - Object containing the selected tool and subtool
|
|
460
|
+
*/
|
|
461
|
+
function handle_subtool_change({
|
|
462
|
+
tool,
|
|
463
|
+
subtool
|
|
464
|
+
}: {
|
|
465
|
+
tool: ToolbarTool;
|
|
466
|
+
subtool: Subtool | null;
|
|
467
|
+
}): void {
|
|
468
|
+
editor.set_subtool(subtool);
|
|
469
|
+
current_subtool = subtool;
|
|
470
|
+
|
|
471
|
+
if (subtool === null) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (tool === "draw") {
|
|
476
|
+
if (subtool === "size") {
|
|
477
|
+
brush_size_visible = true;
|
|
478
|
+
} else if (subtool === "color") {
|
|
479
|
+
brush_color_visible = true;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (tool === "erase" && subtool === "size") {
|
|
484
|
+
eraser_size_visible = true;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (tool === "image" && subtool === "paste") {
|
|
488
|
+
process_clipboard();
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (tool === "image" && subtool === "upload") {
|
|
492
|
+
tick().then(() => {
|
|
493
|
+
disable_click = false;
|
|
494
|
+
open_file_upload();
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
let eraser_size_visible = false;
|
|
500
|
+
let selected_color: ColorInput | string;
|
|
501
|
+
let selected_size: number;
|
|
502
|
+
let selected_opacity = 1;
|
|
503
|
+
let selected_eraser_size: number;
|
|
504
|
+
|
|
505
|
+
$: {
|
|
506
|
+
if (brush_options) {
|
|
507
|
+
update_brush_options();
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (eraser_options) {
|
|
511
|
+
update_eraser_options();
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function update_brush_options(): void {
|
|
516
|
+
const default_color =
|
|
517
|
+
brush_options.default_color === "auto"
|
|
518
|
+
? brush_options.colors[0]
|
|
519
|
+
: brush_options.default_color;
|
|
520
|
+
|
|
521
|
+
// color is already a tuple [color, opacity]
|
|
522
|
+
if (Array.isArray(default_color)) {
|
|
523
|
+
selected_color = default_color[0];
|
|
524
|
+
selected_opacity = default_color[1];
|
|
525
|
+
} else {
|
|
526
|
+
selected_color = default_color;
|
|
527
|
+
|
|
528
|
+
// color is a string, check if it has opacity info
|
|
529
|
+
const color = tinycolor(default_color);
|
|
530
|
+
if (color.getAlpha() < 1) {
|
|
531
|
+
selected_opacity = color.getAlpha();
|
|
532
|
+
} else {
|
|
533
|
+
selected_opacity = 1;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
selected_size =
|
|
538
|
+
typeof brush_options.default_size === "number"
|
|
539
|
+
? brush_options.default_size
|
|
540
|
+
: 25;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function update_eraser_options(): void {
|
|
544
|
+
selected_eraser_size =
|
|
545
|
+
eraser_options.default_size === "auto" ? 25 : eraser_options.default_size;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
let brush_size_visible = false;
|
|
549
|
+
|
|
550
|
+
let brush_color_visible = false;
|
|
551
|
+
|
|
552
|
+
$: brush?.set_brush_color(
|
|
553
|
+
(() => {
|
|
554
|
+
let color_value;
|
|
555
|
+
if (selected_color === "auto") {
|
|
556
|
+
const default_color =
|
|
557
|
+
brush_options.colors.find((color) =>
|
|
558
|
+
Array.isArray(color)
|
|
559
|
+
? color[0] === brush_options.default_color
|
|
560
|
+
: color === brush_options.default_color
|
|
561
|
+
) || brush_options.colors[0];
|
|
562
|
+
|
|
563
|
+
color_value = Array.isArray(default_color)
|
|
564
|
+
? default_color[0]
|
|
565
|
+
: default_color;
|
|
566
|
+
} else {
|
|
567
|
+
color_value = selected_color;
|
|
568
|
+
}
|
|
569
|
+
return color_value;
|
|
570
|
+
})()
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
$: brush?.set_brush_size(
|
|
574
|
+
typeof selected_size === "number" ? selected_size : 25
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
$: brush?.set_eraser_size(
|
|
578
|
+
typeof selected_eraser_size === "number" ? selected_eraser_size : 25
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
$: disable_click =
|
|
582
|
+
current_tool !== "image" ||
|
|
583
|
+
(current_tool === "image" && background_image) ||
|
|
584
|
+
(current_tool === "image" && current_subtool === "webcam") ||
|
|
585
|
+
!sources.includes("upload");
|
|
586
|
+
|
|
587
|
+
let current_subtool: Subtool | null = null;
|
|
588
|
+
let preview = false;
|
|
589
|
+
$: brush?.preview_brush(preview);
|
|
590
|
+
$: brush?.set_brush_opacity(selected_opacity);
|
|
591
|
+
|
|
592
|
+
function handle_zoom_change(zoom_level: number | "fit"): void {
|
|
593
|
+
zoom.set_zoom(zoom_level);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function zoom_in_out(direction: "in" | "out"): void {
|
|
597
|
+
zoom.set_zoom(
|
|
598
|
+
direction === "in"
|
|
599
|
+
? zoom_level + (zoom_level < 1 ? 0.1 : zoom_level * 0.1)
|
|
600
|
+
: zoom_level - (zoom_level < 1 ? 0.1 : zoom_level * 0.1)
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async function process_clipboard(): Promise<void> {
|
|
605
|
+
const items = await navigator.clipboard.read();
|
|
606
|
+
|
|
607
|
+
for (let i = 0; i < items.length; i++) {
|
|
608
|
+
const type = items[i].types.find((t) => t.startsWith("image/"));
|
|
609
|
+
if (type) {
|
|
610
|
+
const blob = await items[i].getType(type);
|
|
611
|
+
|
|
612
|
+
handle_files(blob);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function handle_capture(e: CustomEvent): void {
|
|
618
|
+
if (e.detail !== null) {
|
|
619
|
+
handle_files(e.detail as Blob);
|
|
620
|
+
}
|
|
621
|
+
handle_subtool_change({ tool: current_tool, subtool: null });
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function handle_save(): void {
|
|
625
|
+
dispatch("save");
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
$: add_image_from_url(composite || background);
|
|
629
|
+
$: add_layers_from_url(layers);
|
|
630
|
+
|
|
631
|
+
async function handle_crop_confirm(): Promise<void> {
|
|
632
|
+
const { image } = await crop.get_crop_bounds();
|
|
633
|
+
if (!image) return;
|
|
634
|
+
|
|
635
|
+
await editor.add_image({
|
|
636
|
+
image,
|
|
637
|
+
resize: false
|
|
638
|
+
});
|
|
639
|
+
handle_subtool_change({ tool: "image", subtool: null });
|
|
640
|
+
dispatch("change");
|
|
641
|
+
dispatch("input");
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
async function handle_download(): Promise<void> {
|
|
645
|
+
const blobs = await editor.get_blobs();
|
|
646
|
+
|
|
647
|
+
const blob = blobs.composite;
|
|
648
|
+
if (!blob) {
|
|
649
|
+
dispatch("download_error", "Unable to generate image to download.");
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
const url = URL.createObjectURL(blob);
|
|
653
|
+
const link = document.createElement("a");
|
|
654
|
+
link.href = url;
|
|
655
|
+
link.download = "image.png";
|
|
656
|
+
link.click();
|
|
657
|
+
URL.revokeObjectURL(url);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function handle_undo(): void {
|
|
661
|
+
editor.undo();
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function handle_redo(): void {
|
|
665
|
+
editor.redo();
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
let old_background = background;
|
|
669
|
+
|
|
670
|
+
$: if (old_background !== background) {
|
|
671
|
+
handle_tool_change({ tool: "draw" });
|
|
672
|
+
handle_subtool_change({ tool: "draw", subtool: null });
|
|
673
|
+
current_tool = "draw";
|
|
674
|
+
old_background = background;
|
|
675
|
+
}
|
|
469
676
|
</script>
|
|
470
677
|
|
|
471
678
|
<div
|