@gradio/imageeditor 0.13.0 → 0.14.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/ImageEditor.stories.svelte +9 -1
  3. package/Index.svelte +13 -1
  4. package/InteractiveImageEditor.svelte +10 -1
  5. package/README.md +2 -0
  6. package/dist/Index.svelte +6 -0
  7. package/dist/Index.svelte.d.ts +9 -1
  8. package/dist/InteractiveImageEditor.svelte +7 -0
  9. package/dist/InteractiveImageEditor.svelte.d.ts +5 -1
  10. package/dist/shared/Controls.svelte +13 -1
  11. package/dist/shared/Controls.svelte.d.ts +2 -0
  12. package/dist/shared/ImageEditor.svelte +29 -3
  13. package/dist/shared/ImageEditor.svelte.d.ts +5 -2
  14. package/dist/shared/Layers.svelte +29 -2
  15. package/dist/shared/Layers.svelte.d.ts +2 -0
  16. package/dist/shared/SecondaryToolbar.svelte +1 -0
  17. package/dist/shared/SecondaryToolbar.svelte.d.ts +2 -0
  18. package/dist/shared/brush/brush-textures.d.ts +0 -6
  19. package/dist/shared/brush/brush-textures.js +0 -19
  20. package/dist/shared/brush/brush.js +3 -2
  21. package/dist/shared/core/editor.d.ts +15 -6
  22. package/dist/shared/core/editor.js +76 -33
  23. package/dist/shared/image/image.js +2 -4
  24. package/dist/shared/types.d.ts +4 -0
  25. package/dist/shared/zoom/zoom.js +1 -1
  26. package/package.json +8 -8
  27. package/shared/Controls.svelte +14 -40
  28. package/shared/ImageEditor.svelte +33 -4
  29. package/shared/Layers.svelte +36 -3
  30. package/shared/SecondaryToolbar.svelte +7 -1
  31. package/shared/brush/brush-textures.ts +0 -23
  32. package/shared/brush/brush.ts +6 -2
  33. package/shared/core/editor.ts +102 -44
  34. package/shared/image/image.ts +2 -4
  35. package/shared/types.ts +5 -0
  36. package/shared/zoom/zoom.ts +1 -1
@@ -25,6 +25,7 @@ export declare class CommandManager {
25
25
  export declare class LayerManager {
26
26
  private layers;
27
27
  private active_layer;
28
+ private active_layer_id;
28
29
  private draw_textures;
29
30
  layer_store: Writable<{
30
31
  active_layer: string;
@@ -32,6 +33,7 @@ export declare class LayerManager {
32
33
  name: string;
33
34
  id: string;
34
35
  user_created: boolean;
36
+ visible: boolean;
35
37
  }[];
36
38
  }>;
37
39
  private background_layer;
@@ -42,6 +44,7 @@ export declare class LayerManager {
42
44
  private border_region;
43
45
  private layer_options;
44
46
  constructor(image_container: Container, app: Application, fixed_canvas: boolean, dark: boolean, border_region: number, layer_options: LayerOptions);
47
+ toggle_layer_visibility(id: string): void;
45
48
  create_background_layer(width: number, height: number): Container;
46
49
  set_layer_options(layer_options: LayerOptions, width: number, height: number): void;
47
50
  /**
@@ -53,11 +56,13 @@ export declare class LayerManager {
53
56
  * @returns A Promise that resolves to the background Container when the image is loaded
54
57
  */
55
58
  create_background_layer_from_url(url: string, width?: number, height?: number): Promise<Container>;
56
- create_layer({ width, height, layer_name, user_created }: {
59
+ create_layer({ width, height, layer_name, user_created, layer_id, make_active }: {
57
60
  width: number;
58
61
  height: number;
59
62
  layer_name?: string;
60
63
  user_created: boolean;
64
+ layer_id?: string;
65
+ make_active?: boolean;
61
66
  }): Container;
62
67
  /**
63
68
  * Creates a new layer with an image loaded from a URL
@@ -106,6 +111,7 @@ interface ImageEditorOptions {
106
111
  border_region?: number;
107
112
  layer_options?: LayerOptions;
108
113
  pad_bottom?: number;
114
+ theme_mode?: "dark" | "light";
109
115
  }
110
116
  export interface ImageEditorContext {
111
117
  app: Application;
@@ -132,8 +138,8 @@ export interface ImageEditorContext {
132
138
  y: number;
133
139
  };
134
140
  animate?: boolean;
135
- }) => void;
136
- execute_command: (command: Command) => void;
141
+ }) => Promise<void>;
142
+ execute_command: (command: Command) => Promise<void> | void;
137
143
  resize_canvas: (width: number, height: number) => void;
138
144
  reset: () => void;
139
145
  set_background_image: (image: Sprite) => void;
@@ -170,6 +176,7 @@ export declare class ImageEditor {
170
176
  name: string;
171
177
  id: string;
172
178
  user_created: boolean;
179
+ visible: boolean;
173
180
  }[];
174
181
  }>;
175
182
  private outline_container;
@@ -184,6 +191,7 @@ export declare class ImageEditor {
184
191
  private overlay_container;
185
192
  private overlay_graphics;
186
193
  private pad_bottom;
194
+ private theme_mode;
187
195
  constructor(options: ImageEditorOptions);
188
196
  private get context();
189
197
  init(): Promise<void>;
@@ -199,8 +207,8 @@ export declare class ImageEditor {
199
207
  y: number;
200
208
  };
201
209
  animate?: boolean;
202
- }): void;
203
- execute_command(command: Command): void;
210
+ }): Promise<void>;
211
+ execute_command(command: Command): Promise<void>;
204
212
  undo(): void;
205
213
  redo(): void;
206
214
  add_image({ image, resize }: {
@@ -215,7 +223,7 @@ export declare class ImageEditor {
215
223
  set_tool(tool: ToolbarTool): void;
216
224
  set_subtool(subtool: Subtool): void;
217
225
  set_background_image(image: Sprite): void;
218
- reset_canvas(): void;
226
+ reset_canvas(): Promise<void>;
219
227
  add_layer(): void;
220
228
  /**
221
229
  * Adds a new layer with an image loaded from a URL
@@ -248,5 +256,6 @@ export declare class ImageEditor {
248
256
  }>;
249
257
  get background_image_sprite(): Sprite | undefined;
250
258
  set_layer_options(layer_options: LayerOptions): void;
259
+ toggle_layer_visibility(id: string): void;
251
260
  }
252
261
  export {};
@@ -34,6 +34,7 @@ export class CommandManager {
34
34
  export class LayerManager {
35
35
  layers = [];
36
36
  active_layer = null;
37
+ active_layer_id = null;
37
38
  draw_textures = new Map();
38
39
  layer_store = writable({
39
40
  active_layer: "",
@@ -54,6 +55,17 @@ export class LayerManager {
54
55
  this.border_region = border_region;
55
56
  this.layer_options = layer_options;
56
57
  }
58
+ toggle_layer_visibility(id) {
59
+ const layer = this.layers.find((l) => l.id === id);
60
+ if (layer) {
61
+ layer.container.visible = !layer.container.visible;
62
+ layer.visible = layer.container.visible;
63
+ this.layer_store.update((state) => ({
64
+ active_layer: state.active_layer,
65
+ layers: this.layers
66
+ }));
67
+ }
68
+ }
57
69
  create_background_layer(width, height) {
58
70
  if (this.background_layer) {
59
71
  this.background_layer.destroy();
@@ -171,15 +183,16 @@ export class LayerManager {
171
183
  return layer;
172
184
  }
173
185
  }
174
- create_layer({ width, height, layer_name, user_created }) {
186
+ create_layer({ width, height, layer_name, user_created, layer_id = undefined, make_active = false }) {
175
187
  const layer = new Container();
176
- const layer_id = Math.random().toString(36).substring(2, 15);
188
+ const _layer_id = layer_id || Math.random().toString(36).substring(2, 15);
177
189
  const _layer_name = layer_name || `Layer ${this.layers.length + 1}`;
178
190
  this.layers.push({
179
191
  name: _layer_name,
180
- id: layer_id,
192
+ id: _layer_id,
181
193
  container: layer,
182
- user_created
194
+ user_created,
195
+ visible: true
183
196
  });
184
197
  this.image_container.addChild(layer);
185
198
  // Create a single render texture for drawing with transparency
@@ -205,9 +218,11 @@ export class LayerManager {
205
218
  // Store texture for drawing
206
219
  this.draw_textures.set(layer, draw_texture);
207
220
  this.update_layer_order();
208
- this.active_layer = layer;
221
+ if (make_active) {
222
+ this.set_active_layer(_layer_id);
223
+ }
209
224
  this.layer_store.set({
210
- active_layer: layer_id,
225
+ active_layer: this.active_layer_id || "",
211
226
  layers: this.layers
212
227
  });
213
228
  return layer;
@@ -299,6 +314,7 @@ export class LayerManager {
299
314
  this.layers.find((l) => l.id === id)?.container ||
300
315
  this.layers[0]?.container ||
301
316
  null;
317
+ this.active_layer_id = id;
302
318
  this.layer_store.set({
303
319
  active_layer: id,
304
320
  layers: this.layers
@@ -329,8 +345,9 @@ export class LayerManager {
329
345
  }
330
346
  this.layers[index].container.destroy();
331
347
  if (this.active_layer === this.layers[index].container) {
332
- this.active_layer =
333
- this.layers[Math.max(0, index - 1)]?.container || null;
348
+ const new_active_layer = this.layers[Math.max(0, index - 1)] || null;
349
+ this.active_layer = new_active_layer?.container || null;
350
+ this.active_layer_id = new_active_layer?.id || null;
334
351
  }
335
352
  this.layers = this.layers.filter((l) => l.id !== id);
336
353
  this.layer_store.update((_layers) => ({
@@ -599,19 +616,30 @@ export class LayerManager {
599
616
  }
600
617
  reset_layers(width, height, persist = false) {
601
618
  const _layers_to_recreate = persist
602
- ? this.layers.map((layer) => layer.name)
603
- : this.layer_options.layers;
619
+ ? this.layers.map((layer) => [layer.name, layer.id])
620
+ : this.layer_options.layers.map((layer) => [layer, undefined]);
604
621
  this.layers.forEach((layer) => {
605
622
  this.delete_layer(layer.id);
606
623
  });
607
- for (const layer of _layers_to_recreate) {
624
+ for (const [layer_name, layer_id] of _layers_to_recreate) {
608
625
  this.create_layer({
609
626
  width,
610
627
  height,
611
- layer_name: layer,
612
- user_created: this.layer_options.layers.includes(layer) ? false : true
628
+ layer_name: layer_name,
629
+ user_created: this.layer_options.layers.find((l) => l === layer_name)
630
+ ? false
631
+ : true,
632
+ layer_id: layer_id
613
633
  });
614
634
  }
635
+ if (!persist) {
636
+ this.active_layer = this.layers[0].container;
637
+ this.active_layer_id = this.layers[0].id;
638
+ }
639
+ this.layer_store.update((state) => ({
640
+ active_layer: this.active_layer_id || this.layers[0].id,
641
+ layers: this.layers
642
+ }));
615
643
  }
616
644
  init_layers(width, height) {
617
645
  for (const layer of this.layers) {
@@ -626,8 +654,9 @@ export class LayerManager {
626
654
  });
627
655
  }
628
656
  this.active_layer = this.layers[0].container;
657
+ this.active_layer_id = this.layers[0].id;
629
658
  this.layer_store.update((_layers) => ({
630
- active_layer: _layers.layers[0].id,
659
+ active_layer: this.layers[0].id,
631
660
  layers: this.layers
632
661
  }));
633
662
  }
@@ -739,14 +768,15 @@ export class ImageEditor {
739
768
  overlay_container;
740
769
  overlay_graphics;
741
770
  pad_bottom;
771
+ theme_mode;
742
772
  constructor(options) {
743
773
  this.pad_bottom = options.pad_bottom || 0;
744
774
  this.dark = options.dark || false;
775
+ this.theme_mode = options.theme_mode || "dark";
745
776
  this.target_element = options.target_element;
746
777
  this.width = options.width;
747
778
  this.height = options.height;
748
779
  this.command_manager = new CommandManager();
749
- // this.layers = writable([]);
750
780
  this.ready = new Promise((resolve) => {
751
781
  this.ready_resolve = resolve;
752
782
  });
@@ -816,7 +846,7 @@ export class ImageEditor {
816
846
  width: container_box.width,
817
847
  height: container_box.height,
818
848
  backgroundAlpha: this.dark ? 0 : 1,
819
- backgroundColor: this.dark ? 0x333333 : 0xffffff,
849
+ backgroundColor: this.theme_mode === "dark" ? "#27272a" : "#ffffff",
820
850
  resolution: window.devicePixelRatio,
821
851
  autoDensity: true,
822
852
  antialias: true,
@@ -980,11 +1010,6 @@ export class ImageEditor {
980
1010
  this.layer_manager = new LayerManager(this.image_container, this.app, this.fixed_canvas, this.dark, this.border_region, this.layer_options);
981
1011
  this.layers = this.layer_manager.layer_store;
982
1012
  }
983
- // private resize_image(width: number, height: number): void {
984
- // // Implement image resizing logic here
985
- // this.image_container.width = width;
986
- // this.image_container.height = height;
987
- // }
988
1013
  resize_canvas(width, height) {
989
1014
  if (this.app.renderer) {
990
1015
  this.app.renderer.resize(width, height);
@@ -1006,22 +1031,31 @@ export class ImageEditor {
1006
1031
  zoom.setup(this.context, this.current_tool, this.current_subtool);
1007
1032
  }
1008
1033
  }
1009
- set_image_properties(properties) {
1034
+ async set_image_properties(properties) {
1010
1035
  let hard = typeof properties.animate !== "undefined" ? !properties.animate : true;
1011
1036
  if (properties.position) {
1012
- this.position.set(properties.position, { hard });
1037
+ const pos = this.position.set(properties.position, { hard });
1038
+ if (hard) {
1039
+ await pos;
1040
+ }
1013
1041
  }
1014
1042
  if (properties.scale) {
1015
- this.scale.set(properties.scale, { hard });
1043
+ const scale = this.scale.set(properties.scale, { hard });
1044
+ if (hard) {
1045
+ await scale;
1046
+ }
1016
1047
  }
1017
1048
  if (properties.width && properties.height) {
1018
1049
  this.width = properties.width;
1019
1050
  this.height = properties.height;
1020
- this.dimensions.set({ width: properties.width, height: properties.height }, { hard });
1051
+ const dimensions = this.dimensions.set({ width: properties.width, height: properties.height }, { hard });
1052
+ if (hard) {
1053
+ await dimensions;
1054
+ }
1021
1055
  }
1022
1056
  }
1023
- execute_command(command) {
1024
- this.command_manager.execute(command);
1057
+ async execute_command(command) {
1058
+ await this.command_manager.execute(command);
1025
1059
  }
1026
1060
  undo() {
1027
1061
  this.command_manager.undo();
@@ -1032,9 +1066,12 @@ export class ImageEditor {
1032
1066
  this.notify("change");
1033
1067
  }
1034
1068
  async add_image({ image, resize = true }) {
1035
- const image_command = new AddImageCommand(this.context, image, this.fixed_canvas, this.border_region);
1036
- await image_command.start();
1037
- this.context.execute_command(image_command);
1069
+ const image_tool = this.tools.get("image");
1070
+ await image_tool.add_image({
1071
+ image,
1072
+ fixed_canvas: this.fixed_canvas,
1073
+ border_region: this.border_region
1074
+ });
1038
1075
  }
1039
1076
  /**
1040
1077
  * Adds an image from a URL as the background layer
@@ -1071,13 +1108,13 @@ export class ImageEditor {
1071
1108
  set_background_image(image) {
1072
1109
  this.background_image = image;
1073
1110
  }
1074
- reset_canvas() {
1111
+ async reset_canvas() {
1075
1112
  this.layer_manager.reset_layers(this.width, this.height);
1076
1113
  // Clear background image
1077
1114
  this.background_image = undefined;
1078
1115
  this.background_image_present.set(false);
1079
1116
  // Reset position, scale and dimensions to default values
1080
- this.set_image_properties({
1117
+ await this.set_image_properties({
1081
1118
  width: this.width,
1082
1119
  height: this.height,
1083
1120
  scale: 1,
@@ -1106,7 +1143,9 @@ export class ImageEditor {
1106
1143
  width: this.width,
1107
1144
  height: this.height,
1108
1145
  layer_name: undefined,
1109
- user_created: true
1146
+ user_created: true,
1147
+ layer_id: undefined,
1148
+ make_active: true
1110
1149
  });
1111
1150
  this.notify("change");
1112
1151
  }
@@ -1130,6 +1169,7 @@ export class ImageEditor {
1130
1169
  for await (const url of layer_urls) {
1131
1170
  await this.layer_manager.add_layer_from_url(url);
1132
1171
  }
1172
+ this.layer_manager.set_active_layer(_layers[0].id);
1133
1173
  // Update resize tool if present with border region
1134
1174
  const resize_tool = this.tools.get("resize");
1135
1175
  if (resize_tool && typeof resize_tool.set_border_region === "function") {
@@ -1218,4 +1258,7 @@ export class ImageEditor {
1218
1258
  this.layer_options = layer_options;
1219
1259
  this.layer_manager.set_layer_options(layer_options, this.width, this.height);
1220
1260
  }
1261
+ toggle_layer_visibility(id) {
1262
+ this.layer_manager.toggle_layer_visibility(id);
1263
+ }
1221
1264
  }
@@ -22,8 +22,7 @@ export class ImageTool {
22
22
  cleanup() { }
23
23
  async add_image({ image, fixed_canvas, border_region = 0 }) {
24
24
  const image_command = new AddImageCommand(this.context, image, fixed_canvas, border_region);
25
- await image_command.start();
26
- this.context.execute_command(image_command);
25
+ await this.context.execute_command(image_command);
27
26
  }
28
27
  set_tool(tool, subtool) {
29
28
  this.current_tool = tool;
@@ -93,7 +92,7 @@ export class AddImageCommand {
93
92
  // First ensure we have run start() to create the sprite and get dimensions
94
93
  const [width, height] = await this.start();
95
94
  // Update image properties with the original dimensions and center in viewport
96
- this.context.set_image_properties({
95
+ await this.context.set_image_properties({
97
96
  scale: 1, // Start at 1:1 scale
98
97
  position: {
99
98
  x: this.context.app.screen.width / 2,
@@ -115,7 +114,6 @@ export class AddImageCommand {
115
114
  if (this.sprite) {
116
115
  this.sprite.destroy();
117
116
  }
118
- // The background layer will be automatically cleaned up when a new image is added
119
117
  }
120
118
  }
121
119
  /**
@@ -10,3 +10,7 @@ export interface LayerOptions {
10
10
  }
11
11
  export type Source = "upload" | "webcam" | "clipboard";
12
12
  export type Transform = "crop" | "resize";
13
+ export interface WebcamOptions {
14
+ mirror: boolean;
15
+ constraints: Record<string, any>;
16
+ }
@@ -110,7 +110,7 @@ export class ZoomTool {
110
110
  // Setup event listeners
111
111
  this.setup_event_listeners();
112
112
  // Update context with initial values
113
- this.image_editor_context.set_image_properties({
113
+ await this.image_editor_context.set_image_properties({
114
114
  scale: min_zoom,
115
115
  position: { x: initial_x, y: initial_y }
116
116
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradio/imageeditor",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Gradio UI packages",
5
5
  "type": "module",
6
6
  "author": "",
@@ -11,14 +11,14 @@
11
11
  "pixi-filters": "^6.1.0",
12
12
  "pixi.js": "^8.7.3",
13
13
  "tinycolor2": "^1.6.0",
14
- "@gradio/atoms": "^0.15.0",
15
- "@gradio/icons": "^0.11.0",
16
- "@gradio/image": "^0.22.0",
17
- "@gradio/utils": "^0.10.1",
18
- "@gradio/statustracker": "^0.10.7",
19
- "@gradio/upload": "^0.16.0",
14
+ "@gradio/icons": "^0.12.0",
15
+ "@gradio/client": "^1.14.1",
16
+ "@gradio/image": "^0.22.1",
17
+ "@gradio/statustracker": "^0.10.8",
18
+ "@gradio/upload": "^0.16.1",
19
+ "@gradio/atoms": "^0.15.1",
20
20
  "@gradio/wasm": "^0.18.1",
21
- "@gradio/client": "^1.14.1"
21
+ "@gradio/utils": "^0.10.1"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@gradio/preview": "^0.13.0"
@@ -1,62 +1,25 @@
1
1
  <script lang="ts">
2
2
  import { createEventDispatcher } from "svelte";
3
3
  import { IconButton, IconButtonWrapper } from "@gradio/atoms";
4
- import { Check, Trash, ZoomIn, ZoomOut, Pan } from "@gradio/icons";
4
+ import { Check, Trash, ZoomIn, ZoomOut, Pan, Download } from "@gradio/icons";
5
5
 
6
- /**
7
- * Can the current image be saved?
8
- */
9
6
  export let can_save = false;
10
- /**
11
- * Is the current image changeable?
12
- */
13
7
  export let changeable = false;
14
- /**
15
- * The current zoom level.
16
- */
17
8
  export let current_zoom = 1;
18
- /**
19
- * The current tool.
20
- */
21
9
  export let tool: string;
22
- /**
23
- * Is the current zoom level minimum?
24
- */
25
10
  export let min_zoom = true;
11
+ export let enable_download = false;
26
12
 
27
13
  const dispatch = createEventDispatcher<{
28
- /**
29
- * Remove the current image.
30
- */
31
14
  remove_image: void;
32
- /**
33
- * Undo the last action.
34
- */
35
15
  undo: void;
36
- /**
37
- * Redo the last action.
38
- */
39
16
  redo: void;
40
- /**
41
- * Save the current image.
42
- */
43
17
  save: void;
44
- /**
45
- * Zoom in.
46
- */
47
18
  zoom_in: void;
48
- /**
49
- * Zoom out.
50
- */
51
19
  zoom_out: void;
52
- /**
53
- * Set the zoom level.
54
- */
55
20
  set_zoom: number | "fit";
56
- /**
57
- * Pan the image.
58
- */
59
21
  pan: void;
22
+ download: void;
60
23
  }>();
61
24
 
62
25
  let show_zoom_popup = false;
@@ -81,6 +44,17 @@
81
44
  </script>
82
45
 
83
46
  <IconButtonWrapper>
47
+ {#if enable_download}
48
+ <IconButton
49
+ Icon={Download}
50
+ label="Download"
51
+ on:click={(event) => {
52
+ dispatch("download");
53
+ event.stopPropagation();
54
+ }}
55
+ />
56
+ {/if}
57
+
84
58
  <IconButton
85
59
  Icon={Pan}
86
60
  label="Pan"
@@ -22,7 +22,7 @@
22
22
  import { create_drag } from "@gradio/upload";
23
23
  import SecondaryToolbar from "./SecondaryToolbar.svelte";
24
24
  import { Check } from "@gradio/icons";
25
- import type { LayerOptions, Source, Transform } from "./types";
25
+ import type { LayerOptions, Source, Transform, WebcamOptions } from "./types";
26
26
 
27
27
  import { type ImageBlobs } from "./types";
28
28
  import Controls from "./Controls.svelte";
@@ -36,6 +36,7 @@
36
36
  history: CommandManager["current_history"];
37
37
  upload: void;
38
38
  input: void;
39
+ download_error: string;
39
40
  }>();
40
41
 
41
42
  export const antialias = true;
@@ -51,7 +52,6 @@
51
52
  export let eraser_options: Eraser;
52
53
  export let fixed_canvas = false;
53
54
  export let root: string;
54
- export let mirror_webcam = true;
55
55
  export let i18n: I18nFormatter;
56
56
  export let upload: Client["upload"];
57
57
  export let composite: FileData | null;
@@ -60,6 +60,9 @@
60
60
  export let border_region = 0;
61
61
  export let layer_options: LayerOptions;
62
62
  export let current_tool: ToolbarTool;
63
+ export let webcam_options: WebcamOptions;
64
+ export let show_download_button = false;
65
+ export let theme_mode: "dark" | "light";
63
66
 
64
67
  let pixi_target: HTMLDivElement;
65
68
  let pixi_target_crop: HTMLDivElement;
@@ -256,7 +259,8 @@
256
259
  tools: ["image", zoom, new ResizeTool(), brush],
257
260
  fixed_canvas,
258
261
  border_region,
259
- layer_options
262
+ layer_options,
263
+ theme_mode
260
264
  });
261
265
 
262
266
  brush.on("change", () => {
@@ -563,6 +567,22 @@
563
567
  dispatch("change");
564
568
  dispatch("input");
565
569
  }
570
+
571
+ async function handle_download(): Promise<void> {
572
+ const blobs = await editor.get_blobs();
573
+
574
+ const blob = blobs.composite;
575
+ if (!blob) {
576
+ dispatch("download_error", "Unable to generate image to download.");
577
+ return;
578
+ }
579
+ const url = URL.createObjectURL(blob);
580
+ const link = document.createElement("a");
581
+ link.href = url;
582
+ link.download = "image.png";
583
+ link.click();
584
+ URL.revokeObjectURL(url);
585
+ }
566
586
  </script>
567
587
 
568
588
  <div
@@ -599,6 +619,8 @@
599
619
  on:pan={(e) => {
600
620
  handle_tool_change({ tool: "pan" });
601
621
  }}
622
+ enable_download={show_download_button}
623
+ on:download={() => handle_download()}
602
624
  />
603
625
  {/if}
604
626
 
@@ -633,11 +655,12 @@
633
655
  on:capture={handle_capture}
634
656
  on:error
635
657
  on:drag
636
- {mirror_webcam}
637
658
  streaming={false}
638
659
  mode="image"
639
660
  include_audio={false}
640
661
  {i18n}
662
+ mirror_webcam={webcam_options.mirror}
663
+ webcam_constraints={webcam_options.constraints}
641
664
  />
642
665
  </div>
643
666
  </div>
@@ -652,6 +675,9 @@
652
675
  }}
653
676
  on:change_layer={(e) => {
654
677
  editor.set_layer(e.detail);
678
+ if (current_tool === "draw") {
679
+ handle_tool_change({ tool: "draw" });
680
+ }
655
681
  }}
656
682
  on:move_layer={(e) => {
657
683
  editor.move_layer(e.detail.id, e.detail.direction);
@@ -659,6 +685,9 @@
659
685
  on:delete_layer={(e) => {
660
686
  editor.delete_layer(e.detail);
661
687
  }}
688
+ on:toggle_layer_visibility={(e) => {
689
+ editor.toggle_layer_visibility(e.detail);
690
+ }}
662
691
  />
663
692
  {/if}
664
693
  {/if}
@@ -1,6 +1,14 @@
1
1
  <script lang="ts">
2
2
  import { click_outside } from "./utils/events";
3
- import { Layers, Clear, ArrowUp, ArrowDown, Plus } from "@gradio/icons";
3
+ import {
4
+ Layers,
5
+ Clear,
6
+ ArrowUp,
7
+ ArrowDown,
8
+ Plus,
9
+ Visibility,
10
+ VisibilityOff
11
+ } from "@gradio/icons";
4
12
  import { IconButton } from "@gradio/atoms";
5
13
  import { createEventDispatcher } from "svelte";
6
14
  import type { Writable } from "svelte/store";
@@ -10,11 +18,17 @@
10
18
  change_layer: string;
11
19
  move_layer: { id: string; direction: "up" | "down" };
12
20
  delete_layer: string;
21
+ toggle_layer_visibility: string;
13
22
  }>();
14
23
 
15
24
  export let layers: Writable<{
16
25
  active_layer: string;
17
- layers: { name: string; id: string; user_created: boolean }[];
26
+ layers: {
27
+ name: string;
28
+ id: string;
29
+ user_created: boolean;
30
+ visible: boolean;
31
+ }[];
18
32
  }>;
19
33
  export let enable_additional_layers = true;
20
34
  export let enable_layers = true;
@@ -56,8 +70,27 @@
56
70
  </button>
57
71
  {#if show_layers}
58
72
  <ul>
59
- {#each $layers.layers as { id, name, user_created }, i (i)}
73
+ {#each $layers.layers as { id, name, user_created, visible }, i (i)}
60
74
  <li>
75
+ {#if !visible}
76
+ <IconButton
77
+ Icon={VisibilityOff}
78
+ size="small"
79
+ on:click={(e) => {
80
+ e.stopPropagation();
81
+ dispatch("toggle_layer_visibility", id);
82
+ }}
83
+ />
84
+ {:else}
85
+ <IconButton
86
+ Icon={Visibility}
87
+ size="small"
88
+ on:click={(e) => {
89
+ e.stopPropagation();
90
+ dispatch("toggle_layer_visibility", id);
91
+ }}
92
+ />
93
+ {/if}
61
94
  <button
62
95
  class:selected_layer={$layers.active_layer === id}
63
96
  aria-label={`layer-${i + 1}`}