@gradio/imageeditor 0.4.10 → 0.5.0
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 +38 -0
- package/ImageEditor.stories.svelte +6 -2
- package/Index.svelte +39 -13
- package/package.json +8 -8
- package/shared/Controls.svelte +2 -2
- package/shared/ImageEditor.svelte +37 -12
- package/shared/InteractiveImageEditor.svelte +103 -20
- package/shared/layers/Layers.svelte +113 -56
- package/shared/layers/utils.ts +44 -4
- package/shared/tools/Brush.svelte +42 -51
- package/shared/tools/{BrushColor.svelte → BrushOptions.svelte} +73 -32
- package/shared/tools/BrushSize.svelte +13 -20
- package/shared/tools/ColorSwatch.svelte +66 -34
- package/shared/tools/Crop.svelte +12 -17
- package/shared/tools/Cropper.svelte +2 -0
- package/shared/tools/Handle.svelte +1 -1
- package/shared/tools/Sources.svelte +45 -14
- package/shared/tools/Tools.svelte +42 -57
- package/shared/tools/crop.ts +9 -4
- package/shared/utils/events.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# @gradio/imageeditor
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- [#8042](https://github.com/gradio-app/gradio/pull/8042) [`92139f3`](https://github.com/gradio-app/gradio/commit/92139f3d7d6b832b649ff1f6c10c87e6fb522cde) - refresh the `ImageEditor` UI. Thanks @pngwn!
|
|
8
|
+
- [#8059](https://github.com/gradio-app/gradio/pull/8059) [`074ce38`](https://github.com/gradio-app/gradio/commit/074ce3805ada6ba829c17da3af5f186d9f5f9865) - ensure the `ImageEditor` works correctly with layers and `change` events. Thanks @pngwn!
|
|
9
|
+
- [#7845](https://github.com/gradio-app/gradio/pull/7845) [`dbb7373`](https://github.com/gradio-app/gradio/commit/dbb7373dde69d4ed2741942b5a1898f8620cec24) - ensure `ImageEditor` events work as expected. Thanks @pngwn!
|
|
10
|
+
|
|
11
|
+
### Fixes
|
|
12
|
+
|
|
13
|
+
- [#8046](https://github.com/gradio-app/gradio/pull/8046) [`d6c289b`](https://github.com/gradio-app/gradio/commit/d6c289b34668f0011676737ee8edb4cb62b0620a) - round [x, y, w, h] before cropping to avoid unexpected interpolation on pixel values. Thanks @ernestchu!
|
|
14
|
+
- [#7959](https://github.com/gradio-app/gradio/pull/7959) [`2a5cb97`](https://github.com/gradio-app/gradio/commit/2a5cb978240960fd8b150a1eae6ae8328a5c0c59) - ensure `ImageEditor` always draws at the correct position. Thanks @hrrbay!
|
|
15
|
+
|
|
16
|
+
### Dependency updates
|
|
17
|
+
|
|
18
|
+
- @gradio/utils@0.3.2
|
|
19
|
+
- @gradio/statustracker@0.4.12
|
|
20
|
+
- @gradio/client@0.16.0
|
|
21
|
+
- @gradio/upload@0.8.5
|
|
22
|
+
- @gradio/atoms@0.7.0
|
|
23
|
+
- @gradio/icons@0.4.0
|
|
24
|
+
- @gradio/image@0.9.12
|
|
25
|
+
|
|
26
|
+
## 0.4.11
|
|
27
|
+
|
|
28
|
+
### Fixes
|
|
29
|
+
|
|
30
|
+
- [#7817](https://github.com/gradio-app/gradio/pull/7817) [`867ff16`](https://github.com/gradio-app/gradio/commit/867ff16cd47ceb5bbf076503538a262b6fe64638) - Trigger the "clear" event of Image Editor. Thanks @uebian!
|
|
31
|
+
|
|
32
|
+
### Dependency updates
|
|
33
|
+
|
|
34
|
+
- @gradio/utils@0.3.1
|
|
35
|
+
- @gradio/atoms@0.6.2
|
|
36
|
+
- @gradio/statustracker@0.4.11
|
|
37
|
+
- @gradio/upload@0.8.4
|
|
38
|
+
- @gradio/image@0.9.11
|
|
39
|
+
- @gradio/client@0.15.1
|
|
40
|
+
|
|
3
41
|
## 0.4.10
|
|
4
42
|
|
|
5
43
|
### Dependency updates
|
|
@@ -26,7 +26,11 @@
|
|
|
26
26
|
class="image-container"
|
|
27
27
|
style="width: 500px; position: relative;border-radius: var(--radius-lg);overflow: hidden;"
|
|
28
28
|
>
|
|
29
|
-
<ImageEditor
|
|
29
|
+
<ImageEditor
|
|
30
|
+
i18n={get(format)}
|
|
31
|
+
{...args}
|
|
32
|
+
server={{ accept_blobs: () => {} }}
|
|
33
|
+
/>
|
|
30
34
|
</div>
|
|
31
35
|
</Template>
|
|
32
36
|
|
|
@@ -118,7 +122,7 @@
|
|
|
118
122
|
coords: { clientX: 100, clientY: 100 }
|
|
119
123
|
});
|
|
120
124
|
|
|
121
|
-
await userEvent.click(canvas.getByLabelText("
|
|
125
|
+
await userEvent.click(canvas.getByLabelText("Draw button"));
|
|
122
126
|
|
|
123
127
|
var availableColors = document.querySelectorAll(
|
|
124
128
|
"button.color:not(.empty):not(.selected):not(.hidden)"
|
package/Index.svelte
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<svelte:options accessors={true} />
|
|
1
|
+
<svelte:options accessors={true} immutable={true} />
|
|
2
2
|
|
|
3
3
|
<script lang="ts">
|
|
4
4
|
import type { Brush, Eraser } from "./shared/tools/Brush.svelte";
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { Block } from "@gradio/atoms";
|
|
14
14
|
import { StatusTracker } from "@gradio/statustracker";
|
|
15
15
|
import type { LoadingStatus } from "@gradio/statustracker";
|
|
16
|
+
import { tick } from "svelte";
|
|
16
17
|
|
|
17
18
|
export let elem_id = "";
|
|
18
19
|
export let elem_classes: string[] = [];
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
export let show_label: boolean;
|
|
27
28
|
export let show_download_button: boolean;
|
|
28
29
|
export let root: string;
|
|
30
|
+
export let value_is_output = false;
|
|
29
31
|
|
|
30
32
|
export let height: number | undefined;
|
|
31
33
|
export let width: number | undefined;
|
|
@@ -47,16 +49,19 @@
|
|
|
47
49
|
export let eraser: Eraser;
|
|
48
50
|
export let crop_size: [number, number] | `${string}:${string}` | null = null;
|
|
49
51
|
export let transforms: "crop"[] = ["crop"];
|
|
50
|
-
|
|
52
|
+
export let layers = true;
|
|
51
53
|
export let attached_events: string[] = [];
|
|
54
|
+
export let server: {
|
|
55
|
+
accept_blobs: (a: any) => void;
|
|
56
|
+
};
|
|
52
57
|
|
|
53
58
|
export let gradio: Gradio<{
|
|
54
59
|
change: never;
|
|
55
60
|
error: string;
|
|
56
61
|
input: never;
|
|
57
62
|
edit: never;
|
|
58
|
-
stream: never;
|
|
59
63
|
drag: never;
|
|
64
|
+
apply: never;
|
|
60
65
|
upload: never;
|
|
61
66
|
clear: never;
|
|
62
67
|
select: SelectData;
|
|
@@ -64,13 +69,16 @@
|
|
|
64
69
|
}>;
|
|
65
70
|
|
|
66
71
|
let editor_instance: InteractiveImageEditor;
|
|
72
|
+
let image_id: null | string = null;
|
|
73
|
+
|
|
74
|
+
export async function get_value(): Promise<ImageBlobs | { id: string }> {
|
|
75
|
+
if (image_id) {
|
|
76
|
+
const val = { id: image_id };
|
|
77
|
+
image_id = null;
|
|
78
|
+
return val;
|
|
79
|
+
}
|
|
67
80
|
|
|
68
|
-
export async function get_value(): Promise<ImageBlobs> {
|
|
69
|
-
// @ts-ignore
|
|
70
|
-
loading_status = { status: "pending" };
|
|
71
81
|
const blobs = await editor_instance.get_data();
|
|
72
|
-
// @ts-ignore
|
|
73
|
-
loading_status = { status: "complete" };
|
|
74
82
|
|
|
75
83
|
return blobs;
|
|
76
84
|
}
|
|
@@ -79,7 +87,15 @@
|
|
|
79
87
|
|
|
80
88
|
$: value && handle_change();
|
|
81
89
|
|
|
82
|
-
function
|
|
90
|
+
function wait_for_next_frame(): Promise<void> {
|
|
91
|
+
return new Promise((resolve) => {
|
|
92
|
+
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function handle_change(): Promise<void> {
|
|
97
|
+
await wait_for_next_frame();
|
|
98
|
+
|
|
83
99
|
if (
|
|
84
100
|
value &&
|
|
85
101
|
(value.background || value.layers?.length || value.composite)
|
|
@@ -89,8 +105,15 @@
|
|
|
89
105
|
}
|
|
90
106
|
|
|
91
107
|
function handle_save(): void {
|
|
108
|
+
gradio.dispatch("apply");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function handle_history_change(): void {
|
|
92
112
|
gradio.dispatch("change");
|
|
93
|
-
|
|
113
|
+
if (!value_is_output) {
|
|
114
|
+
gradio.dispatch("input");
|
|
115
|
+
tick().then((_) => (value_is_output = false));
|
|
116
|
+
}
|
|
94
117
|
}
|
|
95
118
|
</script>
|
|
96
119
|
|
|
@@ -149,6 +172,8 @@
|
|
|
149
172
|
/>
|
|
150
173
|
|
|
151
174
|
<InteractiveImageEditor
|
|
175
|
+
on:change={() => handle_history_change()}
|
|
176
|
+
bind:image_id
|
|
152
177
|
{crop_size}
|
|
153
178
|
{value}
|
|
154
179
|
bind:this={editor_instance}
|
|
@@ -159,10 +184,8 @@
|
|
|
159
184
|
on:save={(e) => handle_save()}
|
|
160
185
|
on:edit={() => gradio.dispatch("edit")}
|
|
161
186
|
on:clear={() => gradio.dispatch("clear")}
|
|
162
|
-
on:stream={() => gradio.dispatch("stream")}
|
|
163
187
|
on:drag={({ detail }) => (dragging = detail)}
|
|
164
188
|
on:upload={() => gradio.dispatch("upload")}
|
|
165
|
-
on:select={({ detail }) => gradio.dispatch("select", detail)}
|
|
166
189
|
on:share={({ detail }) => gradio.dispatch("share", detail)}
|
|
167
190
|
on:error={({ detail }) => {
|
|
168
191
|
loading_status = loading_status || {};
|
|
@@ -172,9 +195,12 @@
|
|
|
172
195
|
on:error
|
|
173
196
|
{brush}
|
|
174
197
|
{eraser}
|
|
175
|
-
changeable={attached_events.includes("
|
|
198
|
+
changeable={attached_events.includes("apply")}
|
|
176
199
|
i18n={gradio.i18n}
|
|
177
200
|
{transforms}
|
|
201
|
+
accept_blobs={server.accept_blobs}
|
|
202
|
+
{layers}
|
|
203
|
+
status={loading_status?.status}
|
|
178
204
|
></InteractiveImageEditor>
|
|
179
205
|
</Block>
|
|
180
206
|
{/if}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gradio/imageeditor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Gradio UI packages",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "",
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
"@types/tinycolor2": "^1.4.6",
|
|
11
11
|
"pixi.js": "^7.3.2",
|
|
12
12
|
"tinycolor2": "^1.6.0",
|
|
13
|
-
"@gradio/atoms": "^0.
|
|
14
|
-
"@gradio/client": "^0.
|
|
15
|
-
"@gradio/
|
|
16
|
-
"@gradio/
|
|
17
|
-
"@gradio/
|
|
18
|
-
"@gradio/
|
|
19
|
-
"@gradio/utils": "^0.3.
|
|
13
|
+
"@gradio/atoms": "^0.7.0",
|
|
14
|
+
"@gradio/client": "^0.16.0",
|
|
15
|
+
"@gradio/icons": "^0.4.0",
|
|
16
|
+
"@gradio/image": "^0.9.12",
|
|
17
|
+
"@gradio/statustracker": "^0.4.12",
|
|
18
|
+
"@gradio/upload": "^0.8.5",
|
|
19
|
+
"@gradio/utils": "^0.3.2",
|
|
20
20
|
"@gradio/wasm": "^0.10.0"
|
|
21
21
|
},
|
|
22
22
|
"main_changeset": true,
|
package/shared/Controls.svelte
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { createEventDispatcher } from "svelte";
|
|
3
3
|
import { IconButton } from "@gradio/atoms";
|
|
4
|
-
import { Clear, Undo, Redo, Check } from "@gradio/icons";
|
|
4
|
+
import { Clear, Undo, Redo, Check, Trash } from "@gradio/icons";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Can the current image be undone?
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
}}
|
|
57
57
|
/>
|
|
58
58
|
<IconButton
|
|
59
|
-
Icon={
|
|
59
|
+
Icon={Trash}
|
|
60
60
|
label="Clear canvas"
|
|
61
61
|
on:click={(event) => {
|
|
62
62
|
dispatch("remove_image");
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
child_bottom: number;
|
|
29
29
|
}>;
|
|
30
30
|
active_tool: Writable<tool>;
|
|
31
|
+
toolbar_box: Writable<DOMRect>;
|
|
31
32
|
crop: Writable<[number, number, number, number]>;
|
|
32
33
|
position_spring: Spring<{
|
|
33
34
|
x: number;
|
|
@@ -67,11 +68,14 @@
|
|
|
67
68
|
export let bg = false;
|
|
68
69
|
export let sources: ("clipboard" | "webcam" | "upload")[];
|
|
69
70
|
const dispatch = createEventDispatcher<{
|
|
71
|
+
clear?: never;
|
|
70
72
|
save: void;
|
|
73
|
+
change: void;
|
|
71
74
|
}>();
|
|
72
75
|
export let crop_constraint = false;
|
|
73
76
|
|
|
74
77
|
let dimensions = writable(crop_size);
|
|
78
|
+
export let height = 0;
|
|
75
79
|
|
|
76
80
|
let editor_box: EditorContext["editor_box"] = writable({
|
|
77
81
|
parent_width: 0,
|
|
@@ -88,6 +92,8 @@
|
|
|
88
92
|
child_bottom: 0
|
|
89
93
|
});
|
|
90
94
|
|
|
95
|
+
$: height = $editor_box.child_height;
|
|
96
|
+
|
|
91
97
|
const crop = writable<[number, number, number, number]>([0, 0, 1, 1]);
|
|
92
98
|
const position_spring = spring(
|
|
93
99
|
{ x: 0, y: 0 },
|
|
@@ -102,6 +108,8 @@
|
|
|
102
108
|
|
|
103
109
|
const { can_redo, can_undo, current_history } = CommandManager;
|
|
104
110
|
|
|
111
|
+
$: $current_history.previous, dispatch("change");
|
|
112
|
+
|
|
105
113
|
$: {
|
|
106
114
|
history = !!$current_history.previous || $active_tool !== "bg";
|
|
107
115
|
}
|
|
@@ -113,6 +121,7 @@
|
|
|
113
121
|
PartialRecord<context_type, (dimensions?: typeof $dimensions) => void>
|
|
114
122
|
> = writable({});
|
|
115
123
|
const contexts: Writable<context_type[]> = writable([]);
|
|
124
|
+
const toolbar_box: Writable<DOMRect> = writable(new DOMRect());
|
|
116
125
|
|
|
117
126
|
const sort_order = ["bg", "layers", "crop", "draw", "erase"] as const;
|
|
118
127
|
const editor_context = setContext<EditorContext>(EDITOR_KEY, {
|
|
@@ -120,6 +129,7 @@
|
|
|
120
129
|
current_layer: writable(null),
|
|
121
130
|
dimensions,
|
|
122
131
|
editor_box,
|
|
132
|
+
toolbar_box,
|
|
123
133
|
active_tool,
|
|
124
134
|
crop,
|
|
125
135
|
position_spring,
|
|
@@ -237,10 +247,10 @@
|
|
|
237
247
|
return $pixi?.get_blobs(
|
|
238
248
|
$pixi.get_layers(),
|
|
239
249
|
new Rectangle(
|
|
240
|
-
l * $dimensions[0],
|
|
241
|
-
t * $dimensions[1],
|
|
242
|
-
w * $dimensions[0],
|
|
243
|
-
h * $dimensions[1]
|
|
250
|
+
Math.round(l * $dimensions[0]),
|
|
251
|
+
Math.round(t * $dimensions[1]),
|
|
252
|
+
Math.round(w * $dimensions[0]),
|
|
253
|
+
Math.round(h * $dimensions[1])
|
|
244
254
|
),
|
|
245
255
|
$dimensions
|
|
246
256
|
);
|
|
@@ -256,6 +266,7 @@
|
|
|
256
266
|
} else {
|
|
257
267
|
set_tool("bg");
|
|
258
268
|
}
|
|
269
|
+
dispatch("clear");
|
|
259
270
|
}
|
|
260
271
|
|
|
261
272
|
onMount(() => {
|
|
@@ -329,39 +340,53 @@
|
|
|
329
340
|
style:transform="translate({$position_spring.x}px, {$position_spring.y}px)"
|
|
330
341
|
></div>
|
|
331
342
|
</div>
|
|
332
|
-
<
|
|
343
|
+
<div class="tools-wrap">
|
|
344
|
+
<slot />
|
|
345
|
+
</div>
|
|
333
346
|
<div
|
|
334
347
|
class="border"
|
|
335
|
-
style:width="{$crop[2] * $editor_box.child_width}px"
|
|
336
|
-
style:height="{$crop[3] * $editor_box.child_height}px"
|
|
348
|
+
style:width="{$crop[2] * $editor_box.child_width + 1}px"
|
|
349
|
+
style:height="{$crop[3] * $editor_box.child_height + 1}px"
|
|
337
350
|
style:top="{$crop[1] * $editor_box.child_height +
|
|
338
|
-
($editor_box.child_top - $editor_box.parent_top)
|
|
351
|
+
($editor_box.child_top - $editor_box.parent_top) -
|
|
352
|
+
0.5}px"
|
|
339
353
|
style:left="{$crop[0] * $editor_box.child_width +
|
|
340
|
-
($editor_box.child_left - $editor_box.parent_left)
|
|
354
|
+
($editor_box.child_left - $editor_box.parent_left) -
|
|
355
|
+
0.5}px"
|
|
341
356
|
></div>
|
|
342
357
|
</div>
|
|
343
358
|
|
|
344
359
|
<style>
|
|
345
360
|
.wrap {
|
|
361
|
+
display: flex;
|
|
346
362
|
width: 100%;
|
|
347
363
|
height: 100%;
|
|
348
364
|
position: relative;
|
|
349
|
-
display: flex;
|
|
350
365
|
justify-content: center;
|
|
351
|
-
align-items:
|
|
366
|
+
align-items: flex-start;
|
|
352
367
|
}
|
|
353
368
|
.border {
|
|
354
369
|
position: absolute;
|
|
355
370
|
border: var(--block-border-color) 1px solid;
|
|
356
371
|
pointer-events: none;
|
|
372
|
+
border-radius: var(--radius-md);
|
|
357
373
|
}
|
|
358
374
|
|
|
359
375
|
.stage-wrap {
|
|
360
376
|
margin: var(--size-8);
|
|
361
377
|
margin-bottom: var(--size-1);
|
|
378
|
+
border-radius: var(--radius-md);
|
|
379
|
+
overflow: hidden;
|
|
362
380
|
}
|
|
363
381
|
|
|
364
|
-
.
|
|
382
|
+
.tools-wrap {
|
|
383
|
+
display: flex;
|
|
384
|
+
justify-content: center;
|
|
385
|
+
align-items: center;
|
|
386
|
+
padding: 0 var(--spacing-xl) 0 0;
|
|
387
|
+
border: 1px solid var(--block-border-color);
|
|
388
|
+
border-radius: var(--radius-sm);
|
|
389
|
+
margin: var(--spacing-xxl) 0 var(--spacing-xxl) 0;
|
|
365
390
|
}
|
|
366
391
|
|
|
367
392
|
.image-container {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
</script>
|
|
14
14
|
|
|
15
15
|
<script lang="ts">
|
|
16
|
+
import { createEventDispatcher } from "svelte";
|
|
16
17
|
import { type I18nFormatter } from "@gradio/utils";
|
|
17
18
|
import { prepare_files, upload, type FileData } from "@gradio/client";
|
|
18
19
|
|
|
@@ -21,12 +22,12 @@
|
|
|
21
22
|
import { type Brush as IBrush } from "./tools/Brush.svelte";
|
|
22
23
|
import { type Eraser } from "./tools/Brush.svelte";
|
|
23
24
|
|
|
24
|
-
export let brush: IBrush | null;
|
|
25
|
-
export let eraser: Eraser | null;
|
|
26
25
|
import { Tools, Crop, Brush, Sources } from "./tools";
|
|
27
26
|
import { BlockLabel } from "@gradio/atoms";
|
|
28
27
|
import { Image as ImageIcon } from "@gradio/icons";
|
|
29
28
|
|
|
29
|
+
export let brush: IBrush | null;
|
|
30
|
+
export let eraser: Eraser | null;
|
|
30
31
|
export let sources: ("clipboard" | "webcam" | "upload")[];
|
|
31
32
|
export let crop_size: [number, number] | `${string}:${string}` | null = null;
|
|
32
33
|
export let i18n: I18nFormatter;
|
|
@@ -40,6 +41,15 @@
|
|
|
40
41
|
composite: null
|
|
41
42
|
};
|
|
42
43
|
export let transforms: "crop"[] = ["crop"];
|
|
44
|
+
export let layers: boolean;
|
|
45
|
+
export let accept_blobs: (a: any) => void;
|
|
46
|
+
export let status: "pending" | "complete" | "error" = "complete";
|
|
47
|
+
|
|
48
|
+
const dispatch = createEventDispatcher<{
|
|
49
|
+
clear?: never;
|
|
50
|
+
upload?: never;
|
|
51
|
+
change?: never;
|
|
52
|
+
}>();
|
|
43
53
|
|
|
44
54
|
let editor: ImageEditor;
|
|
45
55
|
|
|
@@ -51,6 +61,8 @@
|
|
|
51
61
|
return !!o;
|
|
52
62
|
}
|
|
53
63
|
|
|
64
|
+
$: if (bg) dispatch("upload");
|
|
65
|
+
|
|
54
66
|
export async function get_data(): Promise<ImageBlobs> {
|
|
55
67
|
const blobs = await editor.get_blobs();
|
|
56
68
|
|
|
@@ -102,11 +114,83 @@
|
|
|
102
114
|
let bg = false;
|
|
103
115
|
let history = false;
|
|
104
116
|
|
|
117
|
+
export let image_id: null | string = null;
|
|
118
|
+
|
|
105
119
|
$: editor &&
|
|
106
120
|
editor.set_tool &&
|
|
107
121
|
(sources && sources.length
|
|
108
122
|
? editor.set_tool("bg")
|
|
109
123
|
: editor.set_tool("draw"));
|
|
124
|
+
|
|
125
|
+
type BinaryImages = [string, string, File, number | null][];
|
|
126
|
+
|
|
127
|
+
function nextframe(): Promise<void> {
|
|
128
|
+
return new Promise((resolve) => setTimeout(() => resolve(), 30));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let uploading = false;
|
|
132
|
+
let pending = false;
|
|
133
|
+
async function handle_change(e: CustomEvent<Blob | any>): Promise<void> {
|
|
134
|
+
if (uploading) {
|
|
135
|
+
pending = true;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
uploading = true;
|
|
140
|
+
|
|
141
|
+
await nextframe();
|
|
142
|
+
const blobs = await editor.get_blobs();
|
|
143
|
+
|
|
144
|
+
const images: BinaryImages = [];
|
|
145
|
+
|
|
146
|
+
let id = Math.random().toString(36).substring(2);
|
|
147
|
+
|
|
148
|
+
if (blobs.background)
|
|
149
|
+
images.push([
|
|
150
|
+
id,
|
|
151
|
+
"background",
|
|
152
|
+
new File([blobs.background], "background.png"),
|
|
153
|
+
null
|
|
154
|
+
]);
|
|
155
|
+
if (blobs.composite)
|
|
156
|
+
images.push([
|
|
157
|
+
id,
|
|
158
|
+
"composite",
|
|
159
|
+
new File([blobs.composite], "composite.png"),
|
|
160
|
+
null
|
|
161
|
+
]);
|
|
162
|
+
blobs.layers.forEach((layer, i) => {
|
|
163
|
+
if (layer)
|
|
164
|
+
images.push([
|
|
165
|
+
id as string,
|
|
166
|
+
`layer`,
|
|
167
|
+
new File([layer], `layer_${i}.png`),
|
|
168
|
+
i
|
|
169
|
+
]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await Promise.all(
|
|
173
|
+
images.map(async ([image_id, type, data, index]) => {
|
|
174
|
+
return accept_blobs({
|
|
175
|
+
binary: true,
|
|
176
|
+
data: { file: data, id: image_id, type, index }
|
|
177
|
+
});
|
|
178
|
+
})
|
|
179
|
+
);
|
|
180
|
+
image_id = id;
|
|
181
|
+
dispatch("change");
|
|
182
|
+
|
|
183
|
+
await nextframe();
|
|
184
|
+
uploading = false;
|
|
185
|
+
if (pending) {
|
|
186
|
+
pending = false;
|
|
187
|
+
uploading = false;
|
|
188
|
+
handle_change(e);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let active_mode: "webcam" | "color" | null = null;
|
|
193
|
+
let editor_height = 0;
|
|
110
194
|
</script>
|
|
111
195
|
|
|
112
196
|
<BlockLabel
|
|
@@ -116,23 +200,28 @@
|
|
|
116
200
|
/>
|
|
117
201
|
<ImageEditor
|
|
118
202
|
bind:this={editor}
|
|
203
|
+
bind:height={editor_height}
|
|
119
204
|
{changeable}
|
|
120
205
|
on:save
|
|
206
|
+
on:change={handle_change}
|
|
207
|
+
on:clear={() => dispatch("clear")}
|
|
121
208
|
bind:history
|
|
122
209
|
bind:bg
|
|
123
210
|
{sources}
|
|
124
211
|
crop_constraint={!!crop_constraint}
|
|
125
212
|
>
|
|
126
213
|
<Tools {i18n}>
|
|
127
|
-
{
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
214
|
+
<Layers layer_files={value?.layers || null} enable_layers={layers} />
|
|
215
|
+
|
|
216
|
+
<Sources
|
|
217
|
+
{i18n}
|
|
218
|
+
{root}
|
|
219
|
+
{sources}
|
|
220
|
+
bind:bg
|
|
221
|
+
bind:active_mode
|
|
222
|
+
background_file={value?.background || value?.composite || null}
|
|
223
|
+
></Sources>
|
|
224
|
+
|
|
136
225
|
{#if transforms.includes("crop")}
|
|
137
226
|
<Crop {crop_constraint} />
|
|
138
227
|
{/if}
|
|
@@ -151,10 +240,8 @@
|
|
|
151
240
|
{/if}
|
|
152
241
|
</Tools>
|
|
153
242
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
{#if !bg && !history}
|
|
157
|
-
<div class="empty wrap">
|
|
243
|
+
{#if !bg && !history && active_mode !== "webcam" && status !== "error"}
|
|
244
|
+
<div class="empty wrap" style:height={`${editor_height}px`}>
|
|
158
245
|
{#if sources && sources.length}
|
|
159
246
|
<div>Upload an image</div>
|
|
160
247
|
{/if}
|
|
@@ -175,7 +262,6 @@
|
|
|
175
262
|
flex-direction: column;
|
|
176
263
|
justify-content: center;
|
|
177
264
|
align-items: center;
|
|
178
|
-
|
|
179
265
|
position: absolute;
|
|
180
266
|
height: 100%;
|
|
181
267
|
width: 100%;
|
|
@@ -185,6 +271,7 @@
|
|
|
185
271
|
z-index: var(--layer-top);
|
|
186
272
|
text-align: center;
|
|
187
273
|
color: var(--body-text-color);
|
|
274
|
+
top: var(--size-8);
|
|
188
275
|
}
|
|
189
276
|
|
|
190
277
|
.wrap {
|
|
@@ -192,14 +279,10 @@
|
|
|
192
279
|
flex-direction: column;
|
|
193
280
|
justify-content: center;
|
|
194
281
|
align-items: center;
|
|
195
|
-
min-height: var(--size-60);
|
|
196
282
|
color: var(--block-label-text-color);
|
|
197
283
|
line-height: var(--line-md);
|
|
198
|
-
height: 100%;
|
|
199
|
-
padding-top: var(--size-3);
|
|
200
284
|
font-size: var(--text-lg);
|
|
201
285
|
pointer-events: none;
|
|
202
|
-
transform: translateY(-30px);
|
|
203
286
|
}
|
|
204
287
|
|
|
205
288
|
.or {
|