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