@gradio/imageeditor 0.12.14 → 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 (131) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/Example.svelte +1 -1
  3. package/IMAGE_EDITOR_OVERVIEW.md +224 -0
  4. package/ImageEditor.stories.svelte +13 -5
  5. package/Index.svelte +46 -45
  6. package/{shared/InteractiveImageEditor.svelte → InteractiveImageEditor.svelte} +65 -103
  7. package/README.md +2 -0
  8. package/dist/Example.svelte.d.ts +1 -1
  9. package/dist/Index.svelte +35 -33
  10. package/dist/Index.svelte.d.ts +25 -16
  11. package/dist/{shared/InteractiveImageEditor.svelte → InteractiveImageEditor.svelte} +54 -88
  12. package/dist/{shared/InteractiveImageEditor.svelte.d.ts → InteractiveImageEditor.svelte.d.ts} +17 -15
  13. package/dist/shared/Anchor.svelte +311 -0
  14. package/dist/shared/Anchor.svelte.d.ts +18 -0
  15. package/dist/shared/Controls.svelte +169 -18
  16. package/dist/shared/Controls.svelte.d.ts +9 -2
  17. package/dist/shared/{tools/IconButton.svelte → IconButton.svelte} +19 -3
  18. package/dist/shared/{tools/IconButton.svelte.d.ts → IconButton.svelte.d.ts} +4 -1
  19. package/dist/shared/ImageEditor.svelte +572 -289
  20. package/dist/shared/ImageEditor.svelte.d.ts +54 -56
  21. package/dist/shared/Layers.svelte +260 -0
  22. package/dist/shared/Layers.svelte.d.ts +37 -0
  23. package/dist/shared/Resize.svelte +218 -0
  24. package/dist/shared/Resize.svelte.d.ts +29 -0
  25. package/dist/shared/SecondaryToolbar.svelte +55 -0
  26. package/dist/shared/SecondaryToolbar.svelte.d.ts +37 -0
  27. package/dist/shared/Toolbar.svelte +337 -0
  28. package/dist/shared/Toolbar.svelte.d.ts +42 -0
  29. package/dist/shared/{tools → brush}/BrushOptions.svelte +68 -64
  30. package/dist/shared/{tools → brush}/BrushOptions.svelte.d.ts +9 -9
  31. package/dist/shared/{tools → brush}/BrushSize.svelte +2 -27
  32. package/dist/shared/{tools → brush}/ColorField.svelte +2 -1
  33. package/dist/shared/{tools → brush}/ColorField.svelte.d.ts +1 -0
  34. package/dist/shared/{tools → brush}/ColorPicker.svelte +81 -9
  35. package/dist/shared/{tools → brush}/ColorPicker.svelte.d.ts +1 -0
  36. package/dist/shared/{tools → brush}/ColorSwatch.svelte +78 -38
  37. package/dist/shared/{tools → brush}/ColorSwatch.svelte.d.ts +6 -2
  38. package/dist/shared/brush/brush-cursor.d.ts +101 -0
  39. package/dist/shared/brush/brush-cursor.js +327 -0
  40. package/dist/shared/brush/brush-textures.d.ts +65 -0
  41. package/dist/shared/brush/brush-textures.js +385 -0
  42. package/dist/shared/brush/brush-utils.d.ts +32 -0
  43. package/dist/shared/brush/brush-utils.js +59 -0
  44. package/dist/shared/brush/brush.d.ts +152 -0
  45. package/dist/shared/brush/brush.js +397 -0
  46. package/dist/shared/brush/types.d.ts +37 -0
  47. package/dist/shared/brush/types.js +1 -0
  48. package/dist/shared/core/editor.d.ts +261 -0
  49. package/dist/shared/core/editor.js +1264 -0
  50. package/dist/shared/crop/crop.d.ts +201 -0
  51. package/dist/shared/crop/crop.js +727 -0
  52. package/dist/shared/image/image.d.ts +89 -0
  53. package/dist/shared/image/image.js +183 -0
  54. package/dist/shared/resize/resize.d.ts +270 -0
  55. package/dist/shared/resize/resize.js +1413 -0
  56. package/dist/shared/types.d.ts +16 -0
  57. package/dist/shared/utils/pixi.d.ts +16 -57
  58. package/dist/shared/utils/pixi.js +15 -113
  59. package/dist/shared/zoom/zoom.d.ts +136 -0
  60. package/dist/shared/zoom/zoom.js +548 -0
  61. package/package.json +8 -7
  62. package/shared/Anchor.svelte +384 -0
  63. package/shared/Controls.svelte +178 -37
  64. package/shared/{tools/IconButton.svelte → IconButton.svelte} +21 -4
  65. package/shared/ImageEditor.svelte +712 -350
  66. package/shared/Layers.svelte +282 -0
  67. package/shared/Resize.svelte +239 -0
  68. package/shared/SecondaryToolbar.svelte +67 -0
  69. package/shared/Toolbar.svelte +386 -0
  70. package/shared/brush/BRUSH_TOOL.md +134 -0
  71. package/shared/brush/BrushOptions.svelte +200 -0
  72. package/shared/{tools → brush}/BrushSize.svelte +2 -30
  73. package/shared/{tools → brush}/ColorField.svelte +3 -1
  74. package/shared/{tools → brush}/ColorPicker.svelte +86 -9
  75. package/shared/{tools → brush}/ColorSwatch.svelte +95 -42
  76. package/shared/brush/brush-cursor.ts +408 -0
  77. package/shared/brush/brush-textures.ts +485 -0
  78. package/shared/brush/brush-utils.ts +76 -0
  79. package/shared/brush/brush.ts +510 -0
  80. package/shared/brush/types.ts +41 -0
  81. package/shared/core/EDITOR.md +182 -0
  82. package/shared/core/editor.ts +1788 -0
  83. package/shared/crop/CROP.md +139 -0
  84. package/shared/crop/crop.ts +1027 -0
  85. package/shared/image/IMAGE.md +135 -0
  86. package/shared/image/image.ts +298 -0
  87. package/shared/resize/resize.ts +1894 -0
  88. package/shared/types.ts +19 -0
  89. package/shared/utils/pixi.ts +19 -246
  90. package/shared/zoom/zoom.ts +729 -0
  91. package/dist/shared/layers/Layers.svelte +0 -231
  92. package/dist/shared/layers/Layers.svelte.d.ts +0 -18
  93. package/dist/shared/layers/utils.d.ts +0 -73
  94. package/dist/shared/layers/utils.js +0 -156
  95. package/dist/shared/tools/Brush.svelte +0 -215
  96. package/dist/shared/tools/Brush.svelte.d.ts +0 -29
  97. package/dist/shared/tools/Crop.svelte +0 -155
  98. package/dist/shared/tools/Crop.svelte.d.ts +0 -16
  99. package/dist/shared/tools/Cropper.svelte +0 -271
  100. package/dist/shared/tools/Cropper.svelte.d.ts +0 -41
  101. package/dist/shared/tools/Handle.svelte +0 -260
  102. package/dist/shared/tools/Handle.svelte.d.ts +0 -29
  103. package/dist/shared/tools/Sources.svelte +0 -257
  104. package/dist/shared/tools/Sources.svelte.d.ts +0 -34
  105. package/dist/shared/tools/Tools.svelte +0 -102
  106. package/dist/shared/tools/Tools.svelte.d.ts +0 -48
  107. package/dist/shared/tools/brush.d.ts +0 -62
  108. package/dist/shared/tools/brush.js +0 -133
  109. package/dist/shared/tools/crop.d.ts +0 -15
  110. package/dist/shared/tools/crop.js +0 -154
  111. package/dist/shared/tools/index.d.ts +0 -5
  112. package/dist/shared/tools/index.js +0 -5
  113. package/dist/shared/tools/sources.d.ts +0 -46
  114. package/dist/shared/tools/sources.js +0 -121
  115. package/dist/shared/tools/types.d.ts +0 -1
  116. package/shared/layers/Layers.svelte +0 -250
  117. package/shared/layers/utils.ts +0 -294
  118. package/shared/tools/Brush.svelte +0 -280
  119. package/shared/tools/BrushOptions.svelte +0 -180
  120. package/shared/tools/Crop.svelte +0 -189
  121. package/shared/tools/Cropper.svelte +0 -346
  122. package/shared/tools/Handle.svelte +0 -289
  123. package/shared/tools/Sources.svelte +0 -283
  124. package/shared/tools/Tools.svelte +0 -162
  125. package/shared/tools/brush.ts +0 -247
  126. package/shared/tools/crop.ts +0 -213
  127. package/shared/tools/index.ts +0 -5
  128. package/shared/tools/sources.ts +0 -195
  129. package/shared/tools/types.ts +0 -1
  130. /package/dist/shared/{tools → brush}/BrushSize.svelte.d.ts +0 -0
  131. /package/dist/shared/{tools/types.js → types.js} +0 -0
@@ -1,351 +1,634 @@
1
- <script context="module">import {} from "./utils/pixi";
2
- import {} from "./utils/commands";
3
- export const EDITOR_KEY = Symbol("editor");
4
- import {} from "./tools";
1
+ <script context="module">export const EDITOR_KEY = Symbol("editor");
5
2
  </script>
6
3
 
7
- <script>import { onMount, setContext, createEventDispatcher, tick } from "svelte";
8
- import { writable } from "svelte/store";
9
- import { spring } from "svelte/motion";
10
- import { Rectangle } from "pixi.js";
11
- import { command_manager } from "./utils/commands";
12
- import {} from "./layers/utils";
13
- import { create_pixi_app } from "./utils/pixi";
4
+ <script>import { onMount, createEventDispatcher, tick } from "svelte";
5
+ import Toolbar, {} from "./Toolbar.svelte";
6
+ import { CropTool } from "./crop/crop";
7
+ import { ResizeTool } from "./resize/resize";
8
+ import { Webcam } from "@gradio/image";
9
+ import tinycolor, {} from "tinycolor2";
10
+ import { ZoomTool } from "./zoom/zoom";
11
+ import {} from "./utils/commands";
12
+ import { ImageEditor } from "./core/editor";
13
+ import {} from "./brush/types";
14
+ import { BrushTool } from "./brush/brush";
15
+ import { create_drag } from "@gradio/upload";
16
+ import SecondaryToolbar from "./SecondaryToolbar.svelte";
17
+ import { Check } from "@gradio/icons";
18
+ import {} from "./types";
14
19
  import Controls from "./Controls.svelte";
15
- export let antialias = true;
16
- export let crop_size;
17
- export let changeable = false;
18
- export let history;
19
- export let bg = false;
20
- export let sources;
20
+ import IconButton from "./IconButton.svelte";
21
+ const { drag, open_file_upload } = create_drag();
21
22
  const dispatch = createEventDispatcher();
22
- export let crop_constraint = false;
23
+ export const antialias = true;
24
+ export const full_history = null;
25
+ export let changeable = false;
26
+ export let sources = ["upload", "webcam", "clipboard"];
27
+ export let transforms = ["crop", "resize"];
23
28
  export let canvas_size;
24
- export let parent_height;
25
- export let full_history = null;
26
- $:
27
- orig_canvas_size = canvas_size;
28
- const BASE_DIMENSIONS = canvas_size || [800, 600];
29
- let dimensions = writable(BASE_DIMENSIONS);
30
- export let height = 0;
31
- let editor_box = writable({
32
- parent_width: 0,
33
- parent_height,
34
- parent_top: 0,
35
- parent_left: 0,
36
- parent_right: 0,
37
- parent_bottom: 0,
38
- child_width: 0,
39
- child_height: parent_height,
40
- child_top: 0,
41
- child_left: 0,
42
- child_right: 0,
43
- child_bottom: 0
44
- });
45
- export let canvas_height = void 0;
46
- $:
47
- height = $editor_box.child_height;
29
+ export let is_dragging = false;
30
+ export let background_image = false;
31
+ export let brush_options;
32
+ export let eraser_options;
33
+ export let fixed_canvas = false;
34
+ export let root;
35
+ export let i18n;
36
+ export let upload;
37
+ export let composite;
38
+ export let layers;
39
+ export let background;
40
+ export let border_region = 0;
41
+ export let layer_options;
42
+ export let current_tool;
43
+ export let webcam_options;
44
+ export let show_download_button = false;
45
+ export let theme_mode;
46
+ let pixi_target;
47
+ let pixi_target_crop;
48
48
  $:
49
- canvas_height = $editor_box.child_height + 1;
50
- const crop = writable([0, 0, 1, 1]);
51
- const position_spring = spring(
52
- { x: 0, y: 0 },
53
- {
54
- stiffness: 0.1,
55
- damping: 0.5
49
+ if (layer_options) {
50
+ if (check_if_should_init()) {
51
+ editor.set_layer_options(layer_options);
52
+ refresh_tools();
53
+ }
54
+ }
55
+ function refresh_tools() {
56
+ editor.set_tool(current_tool);
57
+ editor.set_subtool(current_subtool);
58
+ }
59
+ function check_if_should_init() {
60
+ return layer_options && editor && ready;
61
+ }
62
+ export let has_drawn = false;
63
+ export async function get_blobs() {
64
+ if (!editor)
65
+ return { background: null, layers: [], composite: null };
66
+ if (!background_image && !has_drawn && !layers.length)
67
+ return { background: null, layers: [], composite: null };
68
+ const blobs = await editor.get_blobs();
69
+ return blobs;
70
+ }
71
+ let editor;
72
+ export function add_image(image) {
73
+ editor.add_image({ image });
74
+ }
75
+ let pending_bg;
76
+ export async function add_image_from_url(source) {
77
+ if (!editor || !source || !check_if_should_init())
78
+ return;
79
+ let url;
80
+ if (typeof source === "string") {
81
+ url = source;
82
+ } else if (source?.meta?._type === "gradio.FileData" && source?.url) {
83
+ url = source.url;
84
+ } else {
85
+ console.warn("Invalid source provided to add_image_from_url:", source);
86
+ return;
56
87
  }
57
- );
58
- const pixi = writable(null);
59
- const CommandManager = command_manager();
60
- const { can_redo, can_undo, current_history } = CommandManager;
61
- function get_start_history(history2) {
62
- if (history2.previous) {
63
- return get_start_history(history2.previous);
88
+ try {
89
+ pending_bg = editor.add_image_from_url(url);
90
+ let pending_crop = crop.add_image_from_url(url);
91
+ await Promise.all([pending_bg, pending_crop]);
92
+ crop.set_tool("image");
93
+ crop.set_subtool("crop");
94
+ background_image = true;
95
+ dispatch("upload");
96
+ dispatch("input");
97
+ } catch (error) {
98
+ console.error("Error adding image from URL:", error);
64
99
  }
65
- return history2;
66
100
  }
67
- $:
68
- $current_history.previous, dispatch("change");
69
- $:
70
- dispatch("history", get_start_history($current_history));
71
- $: {
72
- history = !!$current_history.previous || $active_tool !== "bg";
101
+ export async function add_layers_from_url(source) {
102
+ if (!editor || !source.length || !check_if_should_init())
103
+ return;
104
+ if (Array.isArray(source) && source.every((item) => item?.meta?._type === "gradio.FileData")) {
105
+ try {
106
+ await pending_bg;
107
+ await editor.add_layers_from_url(source.map((item) => item.url));
108
+ dispatch("change");
109
+ dispatch("input");
110
+ } catch (error) {
111
+ console.error("Error adding layer from URL:", error);
112
+ }
113
+ }
73
114
  }
74
- const is_browser = typeof window !== "undefined";
75
- const active_tool = writable("bg");
76
- const reset_context = writable({});
77
- const init_context = writable({});
78
- const contexts = writable([]);
79
- const toolbar_box = writable(
80
- is_browser ? new DOMRect() : {}
81
- );
82
- const sort_order = ["bg", "layers", "crop", "draw", "erase"];
83
- const editor_context = setContext(EDITOR_KEY, {
84
- pixi,
85
- current_layer: writable(null),
86
- dimensions,
87
- editor_box,
88
- toolbar_box,
89
- active_tool,
90
- crop,
91
- position_spring,
92
- command_manager: CommandManager,
93
- current_history,
94
- register_context: (type, {
95
- reset_fn,
96
- init_fn
97
- }) => {
98
- contexts.update((c) => [...c, type]);
99
- init_context.update((c) => ({ ...c, [type]: init_fn }));
100
- reset_context.update((c) => ({ ...c, [type]: reset_fn }));
101
- },
102
- reset: (clear_image, dimensions2) => {
103
- bg = false;
104
- const _sorted_contexts = $contexts.sort((a, b) => {
105
- return sort_order.indexOf(a) - sort_order.indexOf(b);
115
+ let brush;
116
+ let zoom;
117
+ let zoom_level = 1;
118
+ let ready = false;
119
+ let mounted = false;
120
+ let min_zoom = true;
121
+ let last_dimensions = { width: 0, height: 0 };
122
+ async function handle_visibility_change() {
123
+ if (!editor || !ready || !zoom)
124
+ return;
125
+ await tick();
126
+ const is_visible = pixi_target.offsetParent !== null;
127
+ if (is_visible) {
128
+ const current_dimensions = pixi_target.getBoundingClientRect();
129
+ if (current_dimensions.width !== last_dimensions.width || current_dimensions.height !== last_dimensions.height) {
130
+ zoom.set_zoom("fit");
131
+ last_dimensions = {
132
+ width: current_dimensions.width,
133
+ height: current_dimensions.height
134
+ };
135
+ }
136
+ }
137
+ }
138
+ onMount(() => {
139
+ let intersection_observer;
140
+ let resize_observer;
141
+ init_image_editor().then(() => {
142
+ mounted = true;
143
+ intersection_observer = new IntersectionObserver(() => {
144
+ handle_visibility_change();
106
145
  });
107
- for (const k of _sorted_contexts) {
108
- if (k in $reset_context && typeof $reset_context[k] === "function") {
109
- $reset_context[k]?.();
110
- }
146
+ resize_observer = new ResizeObserver(() => {
147
+ handle_visibility_change();
148
+ });
149
+ intersection_observer.observe(pixi_target);
150
+ resize_observer.observe(pixi_target);
151
+ });
152
+ return () => {
153
+ if (intersection_observer) {
154
+ intersection_observer.disconnect();
111
155
  }
112
- for (const k of _sorted_contexts) {
113
- if (k in $init_context && typeof $init_context[k] === "function") {
114
- if (k === "bg" && !clear_image) {
115
- continue;
116
- } else {
117
- $init_context[k]?.(dimensions2);
118
- }
119
- }
120
- CommandManager.reset();
121
- $pixi?.resize?.(...dimensions2);
156
+ if (resize_observer) {
157
+ resize_observer.disconnect();
122
158
  }
123
- }
159
+ if (editor) {
160
+ editor.destroy();
161
+ }
162
+ };
124
163
  });
125
- let pixi_target;
126
- let canvas_wrap;
127
- function get_dimensions(parent, child) {
128
- if (!parent || !child)
129
- return;
130
- const {
131
- width: parent_width,
132
- height: parent_height2,
133
- top: parent_top,
134
- left: parent_left,
135
- right: parent_right,
136
- bottom: parent_bottom
137
- } = canvas_wrap.getBoundingClientRect();
138
- const {
139
- width: child_width,
140
- height: child_height,
141
- top: child_top,
142
- left: child_left,
143
- right: child_right,
144
- bottom: child_bottom
145
- } = child.getBoundingClientRect();
146
- editor_box.set({
147
- child_width,
148
- child_height,
149
- child_left,
150
- child_right,
151
- child_top,
152
- child_bottom,
153
- parent_width,
154
- parent_height: parent_height2,
155
- parent_left,
156
- parent_right,
157
- parent_top,
158
- parent_bottom
164
+ let crop;
165
+ let crop_zoom;
166
+ async function init_image_editor() {
167
+ brush = new BrushTool();
168
+ zoom = new ZoomTool();
169
+ editor = new ImageEditor({
170
+ target_element: pixi_target,
171
+ width: canvas_size[0],
172
+ height: canvas_size[1],
173
+ tools: ["image", zoom, new ResizeTool(), brush],
174
+ fixed_canvas,
175
+ border_region,
176
+ layer_options,
177
+ theme_mode
178
+ });
179
+ brush.on("change", () => {
180
+ has_drawn = true;
181
+ });
182
+ crop_zoom = new ZoomTool();
183
+ crop = new ImageEditor({
184
+ target_element: pixi_target_crop,
185
+ width: canvas_size[0],
186
+ height: canvas_size[1],
187
+ tools: ["image", crop_zoom, new CropTool()],
188
+ dark: true,
189
+ fixed_canvas: false,
190
+ border_region: 0,
191
+ pad_bottom: 40
192
+ });
193
+ editor.scale.subscribe((_scale) => {
194
+ zoom_level = _scale;
195
+ });
196
+ editor.min_zoom.subscribe((is_min_zoom) => {
197
+ min_zoom = is_min_zoom;
198
+ });
199
+ editor.dimensions.subscribe((dimensions) => {
200
+ last_dimensions = { ...dimensions };
201
+ });
202
+ await Promise.all([editor.ready, crop.ready]).then(() => {
203
+ handle_tool_change({ tool: "image" });
204
+ ready = true;
205
+ if (sources.length > 0) {
206
+ handle_tool_change({ tool: "image" });
207
+ } else {
208
+ handle_tool_change({ tool: "draw" });
209
+ }
210
+ crop.set_subtool("crop");
159
211
  });
212
+ editor.on("change", () => {
213
+ dispatch("change");
214
+ });
215
+ if (background || layers.length > 0) {
216
+ if (background) {
217
+ await add_image_from_url(background);
218
+ }
219
+ if (layers.length > 0) {
220
+ await add_layers_from_url(layers);
221
+ }
222
+ handle_tool_change({ tool: "draw" });
223
+ } else if (composite) {
224
+ await add_image_from_url(composite);
225
+ handle_tool_change({ tool: "draw" });
226
+ }
160
227
  }
161
228
  $:
162
- if (crop_constraint && bg && !history) {
163
- set_crop();
229
+ if (background == null && layers.length == 0 && composite == null && editor && ready) {
230
+ editor.reset_canvas();
231
+ handle_tool_change({ tool: "image" });
232
+ background_image = false;
233
+ has_drawn = false;
164
234
  }
165
- function set_crop() {
166
- requestAnimationFrame(() => {
167
- tick().then((v) => $active_tool = "crop");
168
- });
169
- }
170
- function reposition_canvas() {
171
- if (!$editor_box)
235
+ $:
236
+ current_tool === "image" && current_subtool === "crop" && crop_zoom.set_zoom("fit");
237
+ async function handle_files(files) {
238
+ if (files == null)
172
239
  return;
173
- const [l, t, w, h] = $crop;
174
- const cx = 0;
175
- const cy = 0;
176
- const cw = $editor_box.child_width;
177
- const ch = $editor_box.child_height;
178
- const x = 0.5 * $editor_box.child_width - cx - cw / 2;
179
- const y = 0.5 * $editor_box.child_height - cy - ch / 2;
180
- position_spring.set({ x, y });
181
- }
182
- export async function get_blobs() {
183
- if (!$pixi || !$pixi.get_layers)
184
- return { background: null, layers: [], composite: null };
185
- const [l, t, w, h] = $crop;
186
- return $pixi?.get_blobs(
187
- $pixi.get_layers(),
188
- new Rectangle(
189
- Math.round(0),
190
- Math.round(0),
191
- Math.round(1 * $dimensions[0]),
192
- Math.round(1 * $dimensions[1])
193
- ),
194
- $dimensions
195
- );
240
+ if (!sources.includes("upload"))
241
+ return;
242
+ const _file = Array.isArray(files) ? files[0] : files;
243
+ await editor.add_image({ image: _file });
244
+ await crop.add_image({ image: _file });
245
+ crop.reset();
246
+ background_image = true;
247
+ handle_tool_change({ tool: "draw" });
248
+ dispatch("upload");
249
+ dispatch("input");
250
+ dispatch("change");
196
251
  }
197
- $:
198
- $crop && reposition_canvas();
199
- $:
200
- $position_spring && get_dimensions(canvas_wrap, pixi_target);
201
- export function handle_remove() {
202
- editor_context.reset(
203
- true,
204
- orig_canvas_size ? orig_canvas_size : $dimensions
205
- );
206
- if (!sources.length) {
207
- set_tool("draw");
208
- } else {
209
- set_tool("bg");
252
+ function handle_tool_change({ tool }) {
253
+ editor.set_tool(tool);
254
+ current_tool = tool;
255
+ if (tool === "image") {
256
+ crop.set_tool("image");
257
+ crop.set_subtool("crop");
210
258
  }
211
- dispatch("clear");
212
- let _size = (canvas_size ? canvas_size : crop_size) || [800, 600];
213
- editor_context.reset(true, _size);
214
259
  }
215
- onMount(() => {
216
- const _size = (canvas_size ? canvas_size : crop_size) || [800, 600];
217
- const app = create_pixi_app({
218
- target: pixi_target,
219
- dimensions: _size,
220
- antialias
221
- });
222
- function resize(width, height2) {
223
- app.resize(width, height2);
224
- dimensions.set([width, height2]);
260
+ function handle_subtool_change({
261
+ tool,
262
+ subtool
263
+ }) {
264
+ editor.set_subtool(subtool);
265
+ current_subtool = subtool;
266
+ if (subtool === null) {
267
+ return;
225
268
  }
226
- pixi.set({ ...app, resize });
227
- const resizer = new ResizeObserver((entries) => {
228
- for (const entry of entries) {
229
- get_dimensions(canvas_wrap, pixi_target);
230
- }
231
- });
232
- resizer.observe(canvas_wrap);
233
- resizer.observe(pixi_target);
234
- for (const k of $contexts) {
235
- if (k in $init_context && typeof $init_context[k] === "function") {
236
- $init_context[k]?.($dimensions);
269
+ if (tool === "draw") {
270
+ if (subtool === "size") {
271
+ brush_size_visible = true;
272
+ } else if (subtool === "color") {
273
+ brush_color_visible = true;
237
274
  }
238
275
  }
239
- resize(...$dimensions);
240
- tick().then(() => {
241
- if (full_history) {
242
- CommandManager.hydrate(full_history);
276
+ if (tool === "erase" && subtool === "size") {
277
+ eraser_size_visible = true;
278
+ }
279
+ if (tool === "image" && subtool === "paste") {
280
+ process_clipboard();
281
+ }
282
+ if (tool === "image" && subtool === "upload") {
283
+ tick().then(() => {
284
+ disable_click = false;
285
+ open_file_upload();
286
+ });
287
+ }
288
+ }
289
+ let eraser_size_visible = false;
290
+ let selected_color;
291
+ let selected_size;
292
+ let selected_opacity = 1;
293
+ let selected_eraser_size;
294
+ $: {
295
+ if (brush_options) {
296
+ update_brush_options();
297
+ }
298
+ if (eraser_options) {
299
+ update_eraser_options();
300
+ }
301
+ }
302
+ function update_brush_options() {
303
+ const default_color = brush_options.default_color === "auto" ? brush_options.colors[0] : brush_options.default_color;
304
+ if (Array.isArray(default_color)) {
305
+ selected_color = default_color[0];
306
+ selected_opacity = default_color[1];
307
+ } else {
308
+ selected_color = default_color;
309
+ const color = tinycolor(default_color);
310
+ if (color.getAlpha() < 1) {
311
+ selected_opacity = color.getAlpha();
312
+ } else {
313
+ selected_opacity = 1;
243
314
  }
244
- });
245
- return () => {
246
- $pixi?.destroy();
247
- resizer.disconnect();
248
- for (const k of $contexts) {
249
- if (k in $reset_context) {
250
- $reset_context[k]?.();
315
+ }
316
+ selected_size = typeof brush_options.default_size === "number" ? brush_options.default_size : 25;
317
+ }
318
+ function update_eraser_options() {
319
+ selected_eraser_size = eraser_options.default_size === "auto" ? 25 : eraser_options.default_size;
320
+ }
321
+ let brush_size_visible = false;
322
+ let brush_color_visible = false;
323
+ $:
324
+ brush?.set_brush_color(
325
+ (() => {
326
+ let color_value;
327
+ if (selected_color === "auto") {
328
+ const default_color = brush_options.colors.find(
329
+ (color) => Array.isArray(color) ? color[0] === brush_options.default_color : color === brush_options.default_color
330
+ ) || brush_options.colors[0];
331
+ color_value = Array.isArray(default_color) ? default_color[0] : default_color;
332
+ } else {
333
+ color_value = selected_color;
251
334
  }
335
+ return color_value;
336
+ })()
337
+ );
338
+ $:
339
+ brush?.set_brush_size(
340
+ typeof selected_size === "number" ? selected_size : 25
341
+ );
342
+ $:
343
+ brush?.set_eraser_size(
344
+ typeof selected_eraser_size === "number" ? selected_eraser_size : 25
345
+ );
346
+ $:
347
+ disable_click = current_tool !== "image" || current_tool === "image" && background_image || current_tool === "image" && current_subtool === "webcam" || !sources.includes("upload");
348
+ let current_subtool = null;
349
+ let preview = false;
350
+ $:
351
+ brush?.preview_brush(preview);
352
+ $:
353
+ brush?.set_brush_opacity(selected_opacity);
354
+ function handle_zoom_change(zoom_level2) {
355
+ zoom.set_zoom(zoom_level2);
356
+ }
357
+ function zoom_in_out(direction) {
358
+ zoom.set_zoom(
359
+ direction === "in" ? zoom_level + (zoom_level < 1 ? 0.1 : zoom_level * 0.1) : zoom_level - (zoom_level < 1 ? 0.1 : zoom_level * 0.1)
360
+ );
361
+ }
362
+ async function process_clipboard() {
363
+ const items = await navigator.clipboard.read();
364
+ for (let i = 0; i < items.length; i++) {
365
+ const type = items[i].types.find((t) => t.startsWith("image/"));
366
+ if (type) {
367
+ const blob = await items[i].getType(type);
368
+ handle_files(blob);
252
369
  }
253
- };
254
- });
255
- let saved_history = $current_history;
370
+ }
371
+ }
372
+ function handle_capture(e) {
373
+ if (e.detail !== null) {
374
+ handle_files(e.detail);
375
+ }
376
+ handle_subtool_change({ tool: current_tool, subtool: null });
377
+ }
256
378
  function handle_save() {
257
- saved_history = $current_history;
258
379
  dispatch("save");
259
380
  }
260
- export function set_tool(tool) {
261
- $active_tool = tool;
381
+ $:
382
+ add_image_from_url(composite || background);
383
+ $:
384
+ add_layers_from_url(layers);
385
+ async function handle_crop_confirm() {
386
+ const { image } = await crop.get_crop_bounds();
387
+ if (!image)
388
+ return;
389
+ await editor.add_image({
390
+ image,
391
+ resize: false
392
+ });
393
+ handle_subtool_change({ tool: "image", subtool: null });
394
+ dispatch("change");
395
+ dispatch("input");
396
+ }
397
+ async function handle_download() {
398
+ const blobs = await editor.get_blobs();
399
+ const blob = blobs.composite;
400
+ if (!blob) {
401
+ dispatch("download_error", "Unable to generate image to download.");
402
+ return;
403
+ }
404
+ const url = URL.createObjectURL(blob);
405
+ const link = document.createElement("a");
406
+ link.href = url;
407
+ link.download = "image.png";
408
+ link.click();
409
+ URL.revokeObjectURL(url);
262
410
  }
263
411
  </script>
264
412
 
265
- <svelte:window on:scroll={() => get_dimensions(canvas_wrap, pixi_target)} />
413
+ <div
414
+ data-testid="image"
415
+ class="image-container"
416
+ class:dark-bg={current_subtool === "crop"}
417
+ use:drag={{
418
+ on_drag_change: (dragging) => (is_dragging = dragging),
419
+ on_files: handle_files,
420
+ accepted_types: "image/*",
421
+ disable_click: disable_click
422
+ }}
423
+ aria-label={"Click to upload or drop files"}
424
+ aria-dropeffect="copy"
425
+ >
426
+ {#if ready}
427
+ {#if current_subtool !== "crop"}
428
+ <Controls
429
+ {changeable}
430
+ on:set_zoom={(e) => handle_zoom_change(e.detail)}
431
+ on:zoom_in={() => zoom_in_out("in")}
432
+ on:zoom_out={() => zoom_in_out("out")}
433
+ {min_zoom}
434
+ current_zoom={zoom_level}
435
+ on:remove_image={() => {
436
+ editor.reset_canvas();
437
+ handle_tool_change({ tool: "image" });
438
+ background_image = false;
439
+ has_drawn = false;
440
+ }}
441
+ tool={current_tool}
442
+ can_save={true}
443
+ on:save={handle_save}
444
+ on:pan={(e) => {
445
+ handle_tool_change({ tool: "pan" });
446
+ }}
447
+ enable_download={show_download_button}
448
+ on:download={() => handle_download()}
449
+ />
450
+ {/if}
266
451
 
267
- <div data-testid="image" class="image-container">
268
- <Controls
269
- can_undo={$can_undo}
270
- can_redo={$can_redo}
271
- can_save={saved_history !== $current_history}
272
- {changeable}
273
- on:undo={CommandManager.undo}
274
- on:redo={CommandManager.redo}
275
- on:remove_image={handle_remove}
276
- on:save={handle_save}
277
- />
278
- <div class="container">
279
- <div class="wrap" bind:this={canvas_wrap}>
280
- <div bind:this={pixi_target} class="stage-wrap" class:bg={!bg}></div>
281
- </div>
282
- <div class="tools-wrap">
283
- <slot />
452
+ {#if current_subtool !== "crop"}
453
+ <Toolbar
454
+ {sources}
455
+ {transforms}
456
+ background={background_image}
457
+ on:tool_change={(e) => handle_tool_change(e.detail)}
458
+ on:subtool_change={(e) => handle_subtool_change(e.detail)}
459
+ show_brush_size={brush_size_visible}
460
+ show_brush_color={brush_color_visible}
461
+ show_eraser_size={eraser_size_visible}
462
+ {brush_options}
463
+ {eraser_options}
464
+ bind:selected_color
465
+ bind:selected_size
466
+ bind:selected_eraser_size
467
+ bind:selected_opacity
468
+ bind:preview
469
+ tool={current_tool}
470
+ subtool={current_subtool}
471
+ />
472
+ {/if}
473
+
474
+ {#if current_tool === "image" && current_subtool === "webcam"}
475
+ <div class="modal">
476
+ <div class="modal-inner">
477
+ <Webcam
478
+ {upload}
479
+ {root}
480
+ on:capture={handle_capture}
481
+ on:error
482
+ on:drag
483
+ streaming={false}
484
+ mode="image"
485
+ include_audio={false}
486
+ {i18n}
487
+ mirror_webcam={webcam_options.mirror}
488
+ webcam_constraints={webcam_options.constraints}
489
+ />
490
+ </div>
491
+ </div>
492
+ {/if}
493
+
494
+ {#if current_subtool !== "crop" && !layer_options.disabled}
495
+ <SecondaryToolbar
496
+ enable_additional_layers={layer_options.allow_additional_layers}
497
+ layers={editor.layers}
498
+ on:new_layer={() => {
499
+ editor.add_layer();
500
+ }}
501
+ on:change_layer={(e) => {
502
+ editor.set_layer(e.detail);
503
+ if (current_tool === "draw") {
504
+ handle_tool_change({ tool: "draw" });
505
+ }
506
+ }}
507
+ on:move_layer={(e) => {
508
+ editor.move_layer(e.detail.id, e.detail.direction);
509
+ }}
510
+ on:delete_layer={(e) => {
511
+ editor.delete_layer(e.detail);
512
+ }}
513
+ on:toggle_layer_visibility={(e) => {
514
+ editor.toggle_layer_visibility(e.detail);
515
+ }}
516
+ />
517
+ {/if}
518
+ {/if}
519
+ <div
520
+ class="pixi-target"
521
+ class:visible={current_subtool !== "crop"}
522
+ bind:this={pixi_target}
523
+ ></div>
524
+ <div
525
+ class="pixi-target-crop"
526
+ class:visible={current_subtool === "crop"}
527
+ bind:this={pixi_target_crop}
528
+ ></div>
529
+
530
+ {#if current_subtool === "crop"}
531
+ <div class="crop-confirm-button">
532
+ <IconButton
533
+ Icon={Check}
534
+ label="Confirm crop"
535
+ show_label={true}
536
+ size="large"
537
+ padded={true}
538
+ color="white"
539
+ background="var(--color-green-500)"
540
+ label_position="right"
541
+ on:click={handle_crop_confirm}
542
+ />
284
543
  </div>
285
- <div
286
- class="canvas"
287
- class:no-border={!bg && $active_tool === "bg" && !history}
288
- style:width="{$editor_box.child_width + 1}px"
289
- style:height="{$editor_box.child_height + 1}px"
290
- style:top="{$editor_box.child_top - $editor_box.parent_top - 0.5}px"
291
- style:left="{$editor_box.child_left - $editor_box.parent_left - 0.5}px"
292
- ></div>
293
- </div>
544
+ {/if}
545
+ <slot></slot>
294
546
  </div>
295
547
 
296
548
  <style>
297
- .wrap {
549
+ .image-container {
298
550
  display: flex;
299
- width: 100%;
300
551
  height: 100%;
301
- position: relative;
552
+ flex-direction: column;
302
553
  justify-content: center;
554
+ align-items: center;
555
+ max-height: 100%;
556
+ border-radius: var(--radius-sm);
303
557
  }
304
- .canvas {
558
+
559
+ .pixi-target {
560
+ width: 100%;
561
+ height: 100%;
305
562
  position: absolute;
306
- border: var(--block-border-color) 1px solid;
563
+ top: 0;
564
+ left: 0;
565
+ z-index: 1;
566
+ display: block;
567
+ opacity: 0;
307
568
  pointer-events: none;
308
- border-radius: var(--radius-md);
569
+ border-radius: var(--radius-sm);
309
570
  }
310
571
 
311
- .container {
312
- position: relative;
313
- margin: var(--spacing-md);
314
- /* in case the canvas_size is really small */
315
- /* set min-height so that upload text does not cover the toolbar */
316
- min-height: 100px;
572
+ .pixi-target-crop {
573
+ width: 100%;
574
+ height: 100%;
575
+ position: absolute;
576
+ top: 0;
577
+ left: 0;
578
+ z-index: 2;
579
+ display: block;
580
+ opacity: 0;
581
+ pointer-events: none;
582
+ border-radius: var(--radius-sm);
317
583
  }
318
584
 
319
- .no-border {
320
- border: none;
585
+ .visible {
586
+ opacity: 1;
587
+ pointer-events: auto;
321
588
  }
322
589
 
323
- .stage-wrap {
324
- margin-bottom: var(--size-1);
325
- border-radius: var(--radius-md);
590
+ .pixi-target {
591
+ width: 100%;
592
+ height: 100%;
593
+ position: absolute;
594
+ top: 0;
595
+ left: 0;
596
+ right: 0;
597
+ bottom: 0;
326
598
  overflow: hidden;
327
- height: fit-content;
328
- width: auto;
329
599
  }
330
600
 
331
- .tools-wrap {
601
+ .modal {
602
+ position: absolute;
603
+ height: 100%;
604
+ width: 100%;
605
+ left: 0;
606
+ right: 0;
607
+ margin: auto;
608
+ z-index: var(--layer-top);
332
609
  display: flex;
333
- justify-content: center;
334
610
  align-items: center;
335
- padding: 0 var(--spacing-xl) 0 0;
336
- border: 1px solid var(--block-border-color);
337
- border-radius: var(--radius-sm);
338
- margin: var(--spacing-xxl) 0 var(--spacing-xxl) 0;
339
- width: fit-content;
340
- margin: 0 auto;
341
611
  }
342
612
 
343
- .image-container {
344
- display: flex;
613
+ .modal-inner {
345
614
  height: 100%;
346
- flex-direction: column;
347
- justify-content: center;
615
+ width: 100%;
616
+ background: var(--block-background-fill);
617
+ }
618
+
619
+ .dark-bg {
620
+ background: #333;
621
+ }
622
+
623
+ .crop-confirm-button {
624
+ position: absolute;
625
+ bottom: 8px;
626
+ left: 0;
627
+ right: 0;
628
+ margin: auto;
629
+ z-index: var(--layer-top);
630
+ display: flex;
348
631
  align-items: center;
349
- max-height: 100%;
632
+ justify-content: center;
350
633
  }
351
634
  </style>