@gradio/imageeditor 0.19.0 → 0.20.1

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 (58) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/Example.svelte +9 -3
  3. package/Index.svelte +23 -24
  4. package/InteractiveImageEditor.svelte +99 -61
  5. package/dist/Example.svelte +9 -3
  6. package/dist/Example.svelte.d.ts +4 -18
  7. package/dist/Index.svelte +23 -24
  8. package/dist/Index.svelte.d.ts +2 -19
  9. package/dist/InteractiveImageEditor.svelte +99 -61
  10. package/dist/InteractiveImageEditor.svelte.d.ts +15 -32
  11. package/dist/shared/Anchor.svelte +13 -11
  12. package/dist/shared/Anchor.svelte.d.ts +4 -19
  13. package/dist/shared/Controls.svelte +63 -39
  14. package/dist/shared/Controls.svelte.d.ts +13 -28
  15. package/dist/shared/IconButton.svelte +38 -17
  16. package/dist/shared/IconButton.svelte.d.ts +5 -19
  17. package/dist/shared/ImageEditor.svelte +235 -160
  18. package/dist/shared/ImageEditor.svelte.d.ts +55 -2
  19. package/dist/shared/Layers.svelte +43 -29
  20. package/dist/shared/Layers.svelte.d.ts +10 -25
  21. package/dist/shared/Resize.svelte +22 -16
  22. package/dist/shared/Resize.svelte.d.ts +6 -22
  23. package/dist/shared/SecondaryToolbar.svelte +34 -18
  24. package/dist/shared/SecondaryToolbar.svelte.d.ts +10 -25
  25. package/dist/shared/Toolbar.svelte +60 -76
  26. package/dist/shared/Toolbar.svelte.d.ts +9 -24
  27. package/dist/shared/brush/BrushOptions.svelte +49 -29
  28. package/dist/shared/brush/BrushOptions.svelte.d.ts +6 -21
  29. package/dist/shared/brush/BrushSize.svelte +12 -9
  30. package/dist/shared/brush/BrushSize.svelte.d.ts +5 -20
  31. package/dist/shared/brush/ColorField.svelte +21 -16
  32. package/dist/shared/brush/ColorField.svelte.d.ts +6 -22
  33. package/dist/shared/brush/ColorPicker.svelte +22 -16
  34. package/dist/shared/brush/ColorPicker.svelte.d.ts +4 -18
  35. package/dist/shared/brush/ColorSwatch.svelte +41 -21
  36. package/dist/shared/brush/ColorSwatch.svelte.d.ts +10 -24
  37. package/dist/shared/brush/brush.js +7 -8
  38. package/dist/shared/core/editor.d.ts +5 -0
  39. package/dist/shared/core/editor.js +43 -2
  40. package/dist/shared/resize/resize.d.ts +2 -0
  41. package/dist/shared/resize/resize.js +27 -18
  42. package/package.json +9 -9
  43. package/shared/Anchor.svelte +13 -11
  44. package/shared/Controls.svelte +63 -39
  45. package/shared/IconButton.svelte +38 -17
  46. package/shared/ImageEditor.svelte +235 -160
  47. package/shared/Layers.svelte +43 -29
  48. package/shared/Resize.svelte +22 -16
  49. package/shared/SecondaryToolbar.svelte +34 -18
  50. package/shared/Toolbar.svelte +60 -76
  51. package/shared/brush/BrushOptions.svelte +49 -29
  52. package/shared/brush/BrushSize.svelte +12 -9
  53. package/shared/brush/ColorField.svelte +21 -16
  54. package/shared/brush/ColorPicker.svelte +22 -16
  55. package/shared/brush/ColorSwatch.svelte +41 -21
  56. package/shared/brush/brush.ts +8 -10
  57. package/shared/core/editor.ts +49 -2
  58. package/shared/resize/resize.ts +28 -20
@@ -1,18 +1,21 @@
1
1
  <script lang="ts">
2
2
  import { Eyedropper, Plus } from "@gradio/icons";
3
- import { createEventDispatcher } from "svelte";
4
3
 
5
4
  import tinycolor from "tinycolor2";
6
5
  import IconButton from "../IconButton.svelte";
7
- export let color: string;
8
6
 
9
- export let current_mode: "hex" | "rgb" | "hsl" = "hex";
7
+ let {
8
+ color,
9
+ current_mode = $bindable<"hex" | "rgb" | "hsl">("hex"),
10
+ onselected,
11
+ onclose
12
+ }: {
13
+ color: string;
14
+ current_mode?: "hex" | "rgb" | "hsl";
15
+ onselected?: (color: string) => void;
16
+ onclose?: () => void;
17
+ } = $props();
10
18
 
11
- const dispatch = createEventDispatcher<{
12
- selected: string;
13
- close: void;
14
- add_color: void;
15
- }>();
16
19
  const modes = [
17
20
  ["Hex", "hex"],
18
21
  ["RGB", "rgb"],
@@ -28,8 +31,10 @@
28
31
  return tinycolor(color).toHslString();
29
32
  }
30
33
 
31
- $: color_string = format_color(color, current_mode);
32
- $: color_string && dispatch("selected", color_string);
34
+ let color_string = $derived(format_color(color, current_mode));
35
+ $effect(() => {
36
+ if (color_string) onselected?.(color_string);
37
+ });
33
38
 
34
39
  function request_eyedropper(): void {
35
40
  // @ts-ignore
@@ -44,23 +49,23 @@
44
49
  const eyedropper_supported = !!window.EyeDropper;
45
50
 
46
51
  function handle_click(): void {
47
- dispatch("selected", color_string);
48
- dispatch("close");
52
+ onselected?.(color_string);
53
+ onclose?.();
49
54
  }
50
55
  </script>
51
56
 
52
57
  <div class="input">
53
- <button class="swatch" style:background={color} on:click={handle_click}
58
+ <button class="swatch" style:background={color} onclick={handle_click}
54
59
  ></button>
55
60
  <div>
56
61
  <div class="input-wrap">
57
62
  <input
58
63
  type="text"
59
64
  value={color_string}
60
- on:change={(e) => (color = e.currentTarget.value)}
65
+ onchange={(e) => (color = e.currentTarget.value)}
61
66
  />
62
67
 
63
- <button class="eyedropper" on:click={request_eyedropper}>
68
+ <button class="eyedropper" onclick={request_eyedropper}>
64
69
  {#if eyedropper_supported}
65
70
  <Eyedropper />
66
71
  {/if}
@@ -71,7 +76,7 @@
71
76
  <button
72
77
  class="button"
73
78
  class:active={current_mode === value}
74
- on:click={() => (current_mode = value)}>{label}</button
79
+ onclick={() => (current_mode = value)}>{label}</button
75
80
  >
76
81
  {/each}
77
82
  </div>
@@ -3,19 +3,25 @@
3
3
  import tinycolor from "tinycolor2";
4
4
  import { clamp } from "../utils/pixi";
5
5
 
6
- export let color = "rgb(255, 255, 255)";
7
-
8
- let sl_marker_pos = [0, 0];
6
+ let {
7
+ color = $bindable("rgb(255, 255, 255)"),
8
+ opacity = $bindable(1)
9
+ }: {
10
+ color?: string;
11
+ opacity?: number;
12
+ } = $props();
13
+
14
+ let sl_marker_pos = $state([0, 0]);
9
15
  let sl_rect: DOMRect | null = null;
10
- let sl_moving = false;
11
- let sl = [0, 0];
16
+ let sl_moving = $state(false);
17
+ let sl = $state([0, 0]);
12
18
 
13
- let hue = 0;
14
- let hue_marker_pos = 0;
19
+ let hue = $state(0);
20
+ let hue_marker_pos = $state(0);
15
21
  let hue_rect: DOMRect | null = null;
16
- let hue_moving = false;
22
+ let hue_moving = $state(false);
17
23
 
18
- let opacity_moving = false;
24
+ let opacity_moving = $state(false);
19
25
 
20
26
  function handle_hue_down(
21
27
  event: MouseEvent & { currentTarget: HTMLDivElement }
@@ -121,13 +127,13 @@
121
127
  hue_marker_pos = (hsva.h / 360) * hue_rect!.width;
122
128
  }
123
129
 
124
- $: update_mouse_from_color(color);
130
+ $effect(() => {
131
+ update_mouse_from_color(color);
132
+ });
125
133
 
126
134
  let sl_wrap: HTMLDivElement;
127
135
  let hue_wrap: HTMLDivElement;
128
136
 
129
- export let opacity = 1;
130
-
131
137
  let opacity_wrap: HTMLDivElement;
132
138
 
133
139
  function handle_opacity_down(
@@ -150,12 +156,12 @@
150
156
  });
151
157
  </script>
152
158
 
153
- <svelte:window on:mousemove={handle_move} on:mouseup={handle_end} />
159
+ <svelte:window onmousemove={handle_move} onmouseup={handle_end} />
154
160
 
155
161
  <!-- svelte-ignore a11y-no-static-element-interactions -->
156
162
  <div
157
163
  class="color-gradient"
158
- on:mousedown={handle_sl_down}
164
+ onmousedown={handle_sl_down}
159
165
  style="--hue:{hue}"
160
166
  bind:this={sl_wrap}
161
167
  >
@@ -168,7 +174,7 @@
168
174
  <!-- svelte-ignore a11y-no-static-element-interactions -->
169
175
  <div
170
176
  class="opacity-slider"
171
- on:mousedown={handle_opacity_down}
177
+ onmousedown={handle_opacity_down}
172
178
  style="--color:{color === 'auto' ? 'transparent' : color}"
173
179
  bind:this={opacity_wrap}
174
180
  >
@@ -179,7 +185,7 @@
179
185
  />
180
186
  </div>
181
187
  <!-- svelte-ignore a11y-no-static-element-interactions -->
182
- <div class="hue-slider" on:mousedown={handle_hue_down} bind:this={hue_wrap}>
188
+ <div class="hue-slider" onmousedown={handle_hue_down} bind:this={hue_wrap}>
183
189
  <div
184
190
  class="marker"
185
191
  style:background={"hsl(" + hue + ", 100%, 50%)"}
@@ -1,25 +1,41 @@
1
1
  <script lang="ts">
2
2
  import tinycolor from "tinycolor2";
3
- import { createEventDispatcher } from "svelte";
4
3
  import type { ColorTuple } from "./types";
5
4
  import type { ColorInput } from "tinycolor2";
6
5
  import IconButton from "../IconButton.svelte";
7
6
  import { Plus } from "@gradio/icons";
8
7
 
9
- export let selected_color: string;
10
- export let colors: (ColorInput | ColorTuple)[];
11
- export let user_colors: (ColorInput | ColorTuple)[] | null = [];
12
-
13
- export let show_empty = false;
14
- export let current_mode: "hex" | "rgb" | "hsl" = "hex";
15
- export let color_picker = false;
16
- const dispatch = createEventDispatcher<{
17
- select: { index: number | null; color: string | null; opacity?: number };
18
- edit: { index: number; color: string | null };
19
- add_color: void;
20
- }>();
8
+ let {
9
+ selected_color,
10
+ colors,
11
+ user_colors = [],
12
+ show_empty = false,
13
+ current_mode = "hex",
14
+ color_picker = $bindable(false),
15
+ onselect,
16
+ onedit,
17
+ onadd_color
18
+ }: {
19
+ selected_color: string;
20
+ colors: (ColorInput | ColorTuple)[];
21
+ user_colors?: (ColorInput | ColorTuple)[] | null;
22
+ show_empty?: boolean;
23
+ current_mode?: "hex" | "rgb" | "hsl";
24
+ color_picker?: boolean;
25
+ onselect?: (value: {
26
+ index: number | null;
27
+ color: string | null;
28
+ opacity?: number;
29
+ }) => void;
30
+ onedit?: (value: {
31
+ index: number;
32
+ color: string | null;
33
+ opacity?: number;
34
+ }) => void;
35
+ onadd_color?: () => void;
36
+ } = $props();
21
37
 
22
- $: _colors = show_empty ? colors : colors.filter((c) => c);
38
+ let _colors = $derived(show_empty ? colors : colors.filter((c) => c));
23
39
 
24
40
  function get_color(color_input: ColorInput | ColorTuple): string {
25
41
  if (Array.isArray(color_input)) {
@@ -58,7 +74,7 @@
58
74
  get_formatted_color(selected_color, current_mode)
59
75
  )}`;
60
76
 
61
- $: selected_opacity = get_opacity(selected_color);
77
+ let selected_opacity = $derived(get_opacity(selected_color));
62
78
 
63
79
  function handle_select(
64
80
  type: "edit" | "select",
@@ -69,11 +85,15 @@
69
85
  }
70
86
  ): void {
71
87
  current_index = `${type}-${detail.index}`;
72
- dispatch(type, detail);
88
+ if (type === "edit") {
89
+ onedit?.(detail);
90
+ } else {
91
+ onselect?.(detail);
92
+ }
73
93
  }
74
94
 
75
95
  function handle_picker_click(): void {
76
- dispatch("select", { index: null, color: selected_color });
96
+ onselect?.({ index: null, color: selected_color });
77
97
  color_picker = !color_picker;
78
98
  }
79
99
  </script>
@@ -89,7 +109,7 @@
89
109
  {#if color_picker}
90
110
  <IconButton
91
111
  Icon={Plus}
92
- onclick={() => dispatch("add_color")}
112
+ onclick={() => onadd_color?.()}
93
113
  roundedness="very"
94
114
  background={get_color(selected_color)}
95
115
  color="white"
@@ -99,7 +119,7 @@
99
119
  {@const color_string = get_color(color)}
100
120
  {@const opacity = get_opacity(color)}
101
121
  <button
102
- on:click={() =>
122
+ onclick={() =>
103
123
  handle_select("edit", {
104
124
  index: i,
105
125
  color: color_string,
@@ -115,7 +135,7 @@
115
135
  {/each}
116
136
 
117
137
  <button
118
- on:click={handle_picker_click}
138
+ onclick={handle_picker_click}
119
139
  class="color colorpicker"
120
140
  class:hidden={!color_picker}
121
141
  ></button>
@@ -126,7 +146,7 @@
126
146
  {@const color_string = get_color(color_item)}
127
147
  {@const opacity = get_opacity(color_item)}
128
148
  <button
129
- on:click={() =>
149
+ onclick={() =>
130
150
  handle_select("select", {
131
151
  index: i,
132
152
  color: color_string,
@@ -150,22 +150,23 @@ export class BrushTool implements Tool {
150
150
  * @param {Subtool} subtool - The current subtool.
151
151
  */
152
152
  set_tool(tool: ToolbarTool, subtool: Subtool): void {
153
- this.current_tool = tool;
154
- this.current_subtool = subtool;
153
+ const new_mode = tool === "erase" ? "erase" : "draw";
154
+ const mode_changed = this.state.mode !== new_mode;
155
+ const needs_brush_tool = tool === "erase" || tool === "draw";
155
156
 
156
- if (this.current_tool !== "erase" && this.current_tool !== "draw") {
157
+ if (this.is_drawing && (!needs_brush_tool || mode_changed)) {
157
158
  this.commit_pending_changes();
158
159
  }
159
160
 
161
+ this.current_tool = tool;
162
+ this.current_subtool = subtool;
163
+
160
164
  if (this.brush_cursor) {
161
165
  const should_be_active = tool === "draw" || tool === "erase";
162
166
 
163
167
  this.brush_cursor.set_active(should_be_active);
164
168
  }
165
169
 
166
- const new_mode = tool === "erase" ? "erase" : "draw";
167
- const mode_changed = this.state.mode !== new_mode;
168
- const needs_brush_tool = tool === "erase" || tool === "draw";
169
170
  const textures_initialized =
170
171
  this.brush_textures?.textures_initialized ?? false;
171
172
 
@@ -179,10 +180,7 @@ export class BrushTool implements Tool {
179
180
  Math.round(current_bounds.height) !== Math.round(tex_dims.height);
180
181
  }
181
182
 
182
- if (
183
- needs_brush_tool &&
184
- (mode_changed || !textures_initialized || dimensions_changed)
185
- ) {
183
+ if (needs_brush_tool && (!textures_initialized || dimensions_changed)) {
186
184
  this.brush_textures?.initialize_textures();
187
185
  }
188
186
 
@@ -74,6 +74,7 @@ export interface ImageEditorContext {
74
74
  resize_canvas: (width: number, height: number) => void;
75
75
  reset: () => void;
76
76
  set_background_image: (image: Sprite) => void;
77
+ request_render: () => void;
77
78
  pad_bottom: number;
78
79
  }
79
80
 
@@ -82,6 +83,8 @@ const spring_config = {
82
83
  damping: 0.8
83
84
  };
84
85
 
86
+ const RENDER_IDLE_TIMEOUT_MS = 500;
87
+
85
88
  interface EditorStatePublic {
86
89
  position: { x: number; y: number };
87
90
  scale: number;
@@ -204,6 +207,15 @@ export class ImageEditor {
204
207
  private overlay_graphics!: Graphics;
205
208
  private pad_bottom: number;
206
209
  private theme_mode: "dark" | "light";
210
+ private render_idle_remaining = RENDER_IDLE_TIMEOUT_MS;
211
+ private static readonly WAKE_EVENTS: string[] = [
212
+ "pointerdown",
213
+ "pointermove",
214
+ "pointerup",
215
+ "pointerenter",
216
+ "wheel"
217
+ ];
218
+ private wake_render_loop_bound = this.wake_render_loop.bind(this);
207
219
  constructor(options: ImageEditorOptions) {
208
220
  this.pad_bottom = options.pad_bottom || 0;
209
221
  this.dark = options.dark || false;
@@ -278,7 +290,8 @@ export class ImageEditor {
278
290
  execute_command: this.execute_command.bind(this),
279
291
  resize_canvas: this.resize_canvas.bind(this),
280
292
  reset: this.reset.bind(this),
281
- set_background_image: this.set_background_image.bind(this)
293
+ set_background_image: this.set_background_image.bind(this),
294
+ request_render: this.wake_render_loop.bind(this)
282
295
  };
283
296
  }
284
297
 
@@ -337,17 +350,20 @@ export class ImageEditor {
337
350
  this.dimensions_value = dimensions;
338
351
  this.image_container.width = dimensions.width;
339
352
  this.image_container.height = dimensions.height;
353
+ this.wake_render_loop();
340
354
  });
341
355
 
342
356
  this.scale.subscribe((scale) => {
343
357
  this.scale_value = scale;
358
+ this.wake_render_loop();
344
359
  });
345
360
 
346
361
  this.position.subscribe((position) => {
347
362
  this.position_value = position;
363
+ this.wake_render_loop();
348
364
  });
349
365
 
350
- this.app.ticker.add(() => {
366
+ this.app.ticker.add((ticker) => {
351
367
  this.image_container.scale.set(this.scale_value);
352
368
  this.image_container.position.set(
353
369
  this.position_value.x,
@@ -442,11 +458,28 @@ export class ImageEditor {
442
458
  } else {
443
459
  this.overlay_graphics.clear();
444
460
  }
461
+
462
+ this.render_idle_remaining -= ticker.deltaMS;
463
+ if (this.render_idle_remaining <= 0) {
464
+ ticker.stop();
465
+ }
445
466
  });
446
467
 
468
+ for (const event_name of ImageEditor.WAKE_EVENTS) {
469
+ canvas.addEventListener(event_name, this.wake_render_loop_bound);
470
+ }
471
+
472
+ this.wake_render_loop();
447
473
  this.ready_resolve();
448
474
  }
449
475
 
476
+ private wake_render_loop(): void {
477
+ this.render_idle_remaining = RENDER_IDLE_TIMEOUT_MS;
478
+ if (this.app?.ticker && !this.app.ticker.started) {
479
+ this.app.ticker.start();
480
+ }
481
+ }
482
+
450
483
  private setup_containers(): void {
451
484
  this.image_container = new Container({
452
485
  eventMode: "static",
@@ -542,6 +575,7 @@ export class ImageEditor {
542
575
  if (this.outline_container) {
543
576
  this.outline_container.position.set(app_center_x, app_center_y);
544
577
  }
578
+ this.wake_render_loop();
545
579
  }
546
580
 
547
581
  reset(): void {
@@ -557,6 +591,7 @@ export class ImageEditor {
557
591
  if (brush) {
558
592
  this.set_tool("draw");
559
593
  }
594
+ this.wake_render_loop();
560
595
  }
561
596
 
562
597
  async set_image_properties(properties: {
@@ -647,6 +682,7 @@ export class ImageEditor {
647
682
  for (const tool of this.tools.values()) {
648
683
  tool.set_tool(this.current_tool, this.current_subtool);
649
684
  }
685
+ this.wake_render_loop();
650
686
  }
651
687
 
652
688
  set_subtool(subtool: Subtool): void {
@@ -655,6 +691,7 @@ export class ImageEditor {
655
691
  for (const tool of this.tools.values()) {
656
692
  tool.set_tool(this.current_tool, this.current_subtool);
657
693
  }
694
+ this.wake_render_loop();
658
695
  }
659
696
 
660
697
  set_background_image(image: Sprite): void {
@@ -829,6 +866,7 @@ export class ImageEditor {
829
866
  }
830
867
 
831
868
  private notify(event: "change" | "input"): void {
869
+ this.wake_render_loop();
832
870
  for (const callback of this.event_callbacks.get(event) || []) {
833
871
  callback();
834
872
  }
@@ -837,6 +875,13 @@ export class ImageEditor {
837
875
  destroy(): void {
838
876
  if (!this.app) return;
839
877
 
878
+ const canvas = this.app.canvas as HTMLCanvasElement | undefined;
879
+ if (canvas) {
880
+ for (const event_name of ImageEditor.WAKE_EVENTS) {
881
+ canvas.removeEventListener(event_name, this.wake_render_loop_bound);
882
+ }
883
+ }
884
+
840
885
  this.app?.destroy();
841
886
  }
842
887
 
@@ -877,9 +922,11 @@ export class ImageEditor {
877
922
  this.width,
878
923
  this.height
879
924
  );
925
+ this.wake_render_loop();
880
926
  }
881
927
 
882
928
  toggle_layer_visibility(id: string): void {
883
929
  this.layer_manager.toggle_layer_visibility(id);
930
+ this.wake_render_loop();
884
931
  }
885
932
  }
@@ -108,6 +108,9 @@ export class ResizeTool implements Tool {
108
108
  private dom_mouseup_handler: ((e: MouseEvent) => void) | null = null;
109
109
  private event_callbacks: Map<string, (() => void)[]> = new Map();
110
110
  private last_scale = 1;
111
+ private readonly handle_pointer_move_bound =
112
+ this.handle_pointer_move.bind(this);
113
+ private readonly handle_pointer_up_bound = this.handle_pointer_up.bind(this);
111
114
 
112
115
  private original_state: {
113
116
  width: number;
@@ -223,7 +226,7 @@ export class ResizeTool implements Tool {
223
226
  * @returns {void}
224
227
  */
225
228
  cleanup(): void {
226
- if (this.resize_ui_container) {
229
+ if (this.resize_ui_container && this.image_editor_context?.app) {
227
230
  this.image_editor_context.app.stage.removeChild(this.resize_ui_container);
228
231
  }
229
232
  this.cleanup_event_listeners();
@@ -241,10 +244,12 @@ export class ResizeTool implements Tool {
241
244
  this.current_subtool = subtool;
242
245
 
243
246
  if (tool === "image" && subtool === "size") {
244
- if (this.image_editor_context.background_image) {
245
- const storedBorderRegion = (
246
- this.image_editor_context.background_image as any
247
- ).borderRegion;
247
+ const context = this.image_editor_context;
248
+ if (!context) return;
249
+
250
+ if (context.background_image) {
251
+ const storedBorderRegion = (context.background_image as any)
252
+ .borderRegion;
248
253
  if (typeof storedBorderRegion === "number" && storedBorderRegion > 0) {
249
254
  this.borderRegion = storedBorderRegion;
250
255
  }
@@ -252,9 +257,9 @@ export class ResizeTool implements Tool {
252
257
 
253
258
  this.show_resize_ui();
254
259
 
255
- if (this.image_editor_context.background_image) {
256
- const bg = this.image_editor_context.background_image;
257
- const container = this.image_editor_context.image_container;
260
+ if (context.background_image) {
261
+ const bg = context.background_image;
262
+ const container = context.image_container;
258
263
 
259
264
  const bg_width = bg.width;
260
265
  const bg_height = bg.height;
@@ -936,9 +941,9 @@ export class ResizeTool implements Tool {
936
941
  private setup_event_listeners(): void {
937
942
  const stage = this.image_editor_context.app.stage;
938
943
  stage.eventMode = "static";
939
- stage.on("pointermove", this.handle_pointer_move.bind(this));
940
- stage.on("pointerup", this.handle_pointer_up.bind(this));
941
- stage.on("pointerupoutside", this.handle_pointer_up.bind(this));
944
+ stage.on("pointermove", this.handle_pointer_move_bound);
945
+ stage.on("pointerup", this.handle_pointer_up_bound);
946
+ stage.on("pointerupoutside", this.handle_pointer_up_bound);
942
947
 
943
948
  this.setup_dom_event_listeners();
944
949
  }
@@ -949,7 +954,10 @@ export class ResizeTool implements Tool {
949
954
  private setup_dom_event_listeners(): void {
950
955
  this.cleanup_dom_event_listeners();
951
956
 
952
- const canvas = this.image_editor_context.app.canvas;
957
+ const context = this.image_editor_context;
958
+ if (!context?.app?.canvas) return;
959
+
960
+ const canvas = context.app.canvas;
953
961
 
954
962
  this.dom_mousedown_handler = (e: MouseEvent) => {
955
963
  if (this.current_subtool !== "size") return;
@@ -1070,10 +1078,10 @@ export class ResizeTool implements Tool {
1070
1078
  * Cleans up DOM event listeners
1071
1079
  */
1072
1080
  private cleanup_dom_event_listeners(): void {
1073
- const canvas = this.image_editor_context.app.canvas;
1081
+ const canvas = this.image_editor_context?.app?.canvas;
1074
1082
 
1075
1083
  if (this.dom_mousedown_handler) {
1076
- canvas.removeEventListener("mousedown", this.dom_mousedown_handler);
1084
+ canvas?.removeEventListener("mousedown", this.dom_mousedown_handler);
1077
1085
  this.dom_mousedown_handler = null;
1078
1086
  }
1079
1087
 
@@ -1092,11 +1100,11 @@ export class ResizeTool implements Tool {
1092
1100
  * removes event listeners for the resize tool
1093
1101
  */
1094
1102
  private cleanup_event_listeners(): void {
1095
- const stage = this.image_editor_context.app.stage;
1103
+ const stage = this.image_editor_context?.app?.stage;
1096
1104
 
1097
- stage.off("pointermove", this.handle_pointer_move.bind(this));
1098
- stage.off("pointerup", this.handle_pointer_up.bind(this));
1099
- stage.off("pointerupoutside", this.handle_pointer_up.bind(this));
1105
+ stage?.off("pointermove", this.handle_pointer_move_bound);
1106
+ stage?.off("pointerup", this.handle_pointer_up_bound);
1107
+ stage?.off("pointerupoutside", this.handle_pointer_up_bound);
1100
1108
 
1101
1109
  this.cleanup_dom_event_listeners();
1102
1110
  }
@@ -1687,7 +1695,7 @@ export class ResizeTool implements Tool {
1687
1695
  this.update_resize_ui();
1688
1696
  }
1689
1697
 
1690
- if (this.image_editor_context.background_image) {
1698
+ if (this.image_editor_context?.background_image) {
1691
1699
  const bg = this.image_editor_context.background_image;
1692
1700
  this.current_cursor = bg.cursor;
1693
1701
  bg.cursor = "move";
@@ -1705,7 +1713,7 @@ export class ResizeTool implements Tool {
1705
1713
  this.resize_ui_container.visible = false;
1706
1714
  }
1707
1715
 
1708
- if (this.image_editor_context.background_image) {
1716
+ if (this.image_editor_context?.background_image) {
1709
1717
  const bg = this.image_editor_context.background_image;
1710
1718
  bg.cursor = this.current_cursor;
1711
1719
  }