@gradio/imageeditor 0.14.4 → 0.15.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 (39) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/InteractiveImageEditor.svelte +4 -7
  3. package/dist/InteractiveImageEditor.svelte +4 -3
  4. package/dist/InteractiveImageEditor.svelte.d.ts +1 -1
  5. package/dist/shared/Controls.svelte +32 -1
  6. package/dist/shared/Controls.svelte.d.ts +2 -0
  7. package/dist/shared/ImageEditor.svelte +23 -1
  8. package/dist/shared/ImageEditor.svelte.d.ts +2 -1
  9. package/dist/shared/brush/brush-cursor.js +0 -42
  10. package/dist/shared/brush/brush-textures.d.ts +19 -2
  11. package/dist/shared/brush/brush-textures.js +120 -82
  12. package/dist/shared/brush/brush.js +0 -52
  13. package/dist/shared/{utils → core}/commands.d.ts +19 -58
  14. package/dist/shared/core/commands.js +70 -0
  15. package/dist/shared/core/editor.d.ts +4 -91
  16. package/dist/shared/core/editor.js +18 -708
  17. package/dist/shared/core/layers.d.ts +143 -0
  18. package/dist/shared/core/layers.js +774 -0
  19. package/dist/shared/image/image.d.ts +14 -4
  20. package/dist/shared/image/image.js +91 -18
  21. package/dist/shared/resize/resize.d.ts +31 -0
  22. package/dist/shared/resize/resize.js +127 -290
  23. package/dist/shared/zoom/zoom.js +0 -51
  24. package/package.json +7 -7
  25. package/shared/Controls.svelte +33 -1
  26. package/shared/ImageEditor.svelte +25 -9
  27. package/shared/Toolbar.svelte +1 -4
  28. package/shared/brush/ColorSwatch.svelte +0 -3
  29. package/shared/brush/brush-cursor.ts +0 -42
  30. package/shared/brush/brush-textures.ts +166 -83
  31. package/shared/brush/brush.ts +0 -53
  32. package/shared/core/commands.ts +118 -0
  33. package/shared/core/editor.ts +33 -957
  34. package/shared/core/layers.ts +1068 -0
  35. package/shared/image/image.ts +139 -21
  36. package/shared/resize/resize.ts +160 -299
  37. package/shared/zoom/zoom.ts +0 -51
  38. package/dist/shared/utils/commands.js +0 -73
  39. package/shared/utils/commands.ts +0 -174
@@ -17,11 +17,9 @@ export class ZoomTool implements Tool {
17
17
 
18
18
  private image_editor_context!: ImageEditorContext;
19
19
 
20
- // Constants
21
20
  private readonly max_zoom = 10;
22
21
  private readonly border_padding = 30;
23
22
 
24
- // State
25
23
  private pad_bottom = 0;
26
24
  private is_pinching = false;
27
25
  private is_dragging = false;
@@ -65,37 +63,30 @@ export class ZoomTool implements Tool {
65
63
  * @returns {void}
66
64
  */
67
65
  set_zoom(zoom_level: number | "fit"): void {
68
- // Calculate the min zoom level
69
66
  const fit_zoom = this.calculate_min_zoom(
70
67
  this.local_dimensions.width,
71
68
  this.local_dimensions.height
72
69
  );
73
70
 
74
- // Determine the target zoom level
75
71
  let target_zoom: number;
76
72
  const is_fit_zoom = zoom_level === "fit";
77
73
  if (is_fit_zoom) {
78
74
  target_zoom = fit_zoom;
79
75
  } else {
80
- // Ensure zoom_level is between 0 and 1, then scale it to the range [min_zoom, max_zoom]
81
76
  target_zoom = Math.max(0, Math.min(this.max_zoom, zoom_level));
82
77
  }
83
78
 
84
- // Get the canvas dimensions
85
79
  const canvas = this.image_editor_context.app.screen;
86
80
  const canvas_width = canvas.width;
87
81
  const canvas_height = canvas.height;
88
82
 
89
- // Determine the center point for zooming
90
83
  let center_point: { x: number; y: number };
91
84
 
92
- // Always use the center of the screen
93
85
  center_point = {
94
86
  x: canvas_width / 2,
95
87
  y: canvas_height / 2
96
88
  };
97
89
 
98
- // Apply the zoom using the existing zoom_to_point method
99
90
  this.zoom_to_point(target_zoom, center_point, true, is_fit_zoom);
100
91
  }
101
92
 
@@ -115,22 +106,17 @@ export class ZoomTool implements Tool {
115
106
  this.current_tool = tool;
116
107
  this.current_subtool = subtool;
117
108
  this.pad_bottom = context.pad_bottom;
118
- // Initialize zoom and position
119
109
  const { width, height } = await this.get_container_dimensions();
120
110
  const fit_zoom = this.calculate_min_zoom(width, height);
121
- // Use fit zoom or 1, whichever is smaller
122
111
  const min_zoom = Math.min(fit_zoom, 1);
123
112
 
124
- // Set initial zoom
125
113
  this.local_scale = min_zoom;
126
114
 
127
- // Use simple center positioning
128
115
  const canvas = this.image_editor_context.app.screen;
129
116
 
130
117
  const center_x = canvas.width / 2;
131
118
  const center_y = canvas.height / 2;
132
119
 
133
- // Calculate initial position to center the scaled image
134
120
  const scaled_width = width * min_zoom;
135
121
  const scaled_height = height * min_zoom;
136
122
  const initial_x = center_x - scaled_width / 2;
@@ -138,10 +124,8 @@ export class ZoomTool implements Tool {
138
124
 
139
125
  this.local_position = { x: initial_x, y: initial_y };
140
126
 
141
- // Setup event listeners
142
127
  this.setup_event_listeners();
143
128
 
144
- // Update context with initial values
145
129
  await this.image_editor_context.set_image_properties({
146
130
  scale: min_zoom,
147
131
  position: { x: initial_x, y: initial_y }
@@ -170,15 +154,12 @@ export class ZoomTool implements Tool {
170
154
 
171
155
  canvas.addEventListener("wheel", this.prevent_default, { passive: false });
172
156
 
173
- // Configure stage for interaction
174
157
  stage.eventMode = "static";
175
158
  stage.hitArea = this.image_editor_context.app.screen;
176
159
 
177
- // Add wheel event with options
178
160
  const wheel_listener = this.handle_wheel.bind(this);
179
161
  stage.addEventListener("wheel", wheel_listener, { passive: false });
180
162
 
181
- // Setup touch/pointer events based on device
182
163
  if ("ontouchstart" in window) {
183
164
  stage.addEventListener(
184
165
  "touchstart",
@@ -219,7 +200,6 @@ export class ZoomTool implements Tool {
219
200
  const scroll_speed = is_trackpad ? 30 : 10;
220
201
 
221
202
  if (event.altKey || event.metaKey) {
222
- // Handle zooming with our new simplified logic
223
203
  const local_point = this.image_editor_context.app.stage.toLocal(
224
204
  event.global
225
205
  );
@@ -227,7 +207,6 @@ export class ZoomTool implements Tool {
227
207
  const new_scale = this.local_scale * (1 + zoom_delta);
228
208
  this.zoom_to_point(new_scale, local_point, true);
229
209
  } else {
230
- // Handle panning with bounds
231
210
  const delta_x = event.deltaX;
232
211
  const delta_y = event.deltaY;
233
212
  const raw_position = {
@@ -244,11 +223,9 @@ export class ZoomTool implements Tool {
244
223
 
245
224
  let final_position = { ...raw_position };
246
225
 
247
- // If image is smaller than available space, center it
248
226
  if (scaled_width <= available_width) {
249
227
  final_position.x = (canvas.width - scaled_width) / 2;
250
228
  } else {
251
- // Apply 50% rule for horizontal bounds
252
229
  const max_offset = canvas.width / 2;
253
230
  const left_bound = max_offset;
254
231
  const right_bound = canvas.width - max_offset - scaled_width;
@@ -262,7 +239,6 @@ export class ZoomTool implements Tool {
262
239
  final_position.y =
263
240
  (canvas.height - this.pad_bottom - scaled_height) / 2;
264
241
  } else {
265
- // Apply 50% rule for vertical bounds, accounting for pad_bottom
266
242
  const max_offset = (canvas.height - this.pad_bottom) / 2;
267
243
  const top_bound = max_offset;
268
244
  const bottom_bound =
@@ -358,17 +334,13 @@ export class ZoomTool implements Tool {
358
334
  return 1;
359
335
  }
360
336
 
361
- // Calculate available space accounting for padding
362
337
  const available_width = viewport_width - this.border_padding * 2;
363
338
  const available_height =
364
339
  viewport_height - this.border_padding * 2 - this.pad_bottom;
365
340
 
366
- // Calculate zoom ratios for both dimensions
367
341
  const width_ratio = available_width / container_width;
368
342
  const height_ratio = available_height / container_height;
369
343
 
370
- // Use the smaller ratio to ensure the image fits in both dimensions
371
- // Note: we no longer cap at 1, as 'fit' can be above 1
372
344
  return Math.min(width_ratio, height_ratio);
373
345
  }
374
346
 
@@ -380,7 +352,6 @@ export class ZoomTool implements Tool {
380
352
  private handle_touch_start(event: FederatedPointerEvent): void {
381
353
  event.preventDefault();
382
354
 
383
- // Cast to any to access TouchEvent properties
384
355
  const touchEvent = event as any;
385
356
 
386
357
  if (touchEvent.touches && touchEvent.touches.length === 2) {
@@ -407,7 +378,6 @@ export class ZoomTool implements Tool {
407
378
  private handle_touch_move(event: FederatedPointerEvent): void {
408
379
  event.preventDefault();
409
380
 
410
- // Cast to any to access TouchEvent properties
411
381
  const touchEvent = event as any;
412
382
 
413
383
  if (
@@ -433,7 +403,6 @@ export class ZoomTool implements Tool {
433
403
  const scale_val = current_distance / this.last_pinch_distance;
434
404
  this.last_pinch_distance = current_distance;
435
405
 
436
- // Update zoom based on pinch
437
406
  this.zoom_to_point(this.local_scale * scale_val, pinch_center);
438
407
  } else if (
439
408
  this.is_dragging &&
@@ -450,7 +419,6 @@ export class ZoomTool implements Tool {
450
419
  const dx = current_position.x - this.last_touch_position.x;
451
420
  const dy = current_position.y - this.last_touch_position.y;
452
421
 
453
- // Update position based on drag
454
422
  this.image_editor_context.set_image_properties({
455
423
  position: {
456
424
  x: this.local_position.x + dx,
@@ -470,7 +438,6 @@ export class ZoomTool implements Tool {
470
438
  private handle_touch_end(event: FederatedPointerEvent): void {
471
439
  event.preventDefault();
472
440
 
473
- // Cast to any to access TouchEvent properties
474
441
  const touchEvent = event as any;
475
442
 
476
443
  if (touchEvent.touches && touchEvent.touches.length < 2) {
@@ -507,7 +474,6 @@ export class ZoomTool implements Tool {
507
474
  const scaled_width = this.local_dimensions.width * this.local_scale;
508
475
  const scaled_height = this.local_dimensions.height * this.local_scale;
509
476
 
510
- // Calculate center position, accounting for pad_bottom
511
477
  const center_position = {
512
478
  x: (canvas.width - scaled_width) / 2,
513
479
  y: (canvas.height - scaled_height - this.pad_bottom) / 2
@@ -555,43 +521,35 @@ export class ZoomTool implements Tool {
555
521
  hard?: boolean,
556
522
  is_fit_zoom?: boolean
557
523
  ): void {
558
- // Get the cursor position relative to the image's top-left corner
559
524
  const cursor_relative_to_image = {
560
525
  x: (point.x - this.local_position.x) / this.local_scale,
561
526
  y: (point.y - this.local_position.y) / this.local_scale
562
527
  };
563
528
 
564
- // Calculate what percentage of the image width/height this represents
565
529
  const image_percentages = {
566
530
  x: cursor_relative_to_image.x / this.local_dimensions.width,
567
531
  y: cursor_relative_to_image.y / this.local_dimensions.height
568
532
  };
569
533
 
570
- // Apply zoom limits
571
534
  const fit_zoom = this.calculate_min_zoom(
572
535
  this.local_dimensions.width,
573
536
  this.local_dimensions.height
574
537
  );
575
- // Use the fit zoom or 1, whichever is smaller as the minimum zoom
576
538
  const min_zoom = Math.min(fit_zoom, 1);
577
539
  new_zoom = Math.min(Math.max(new_zoom, min_zoom), this.max_zoom);
578
540
 
579
- // Calculate new dimensions
580
541
  const new_scaled_width = this.local_dimensions.width * new_zoom;
581
542
  const new_scaled_height = this.local_dimensions.height * new_zoom;
582
543
 
583
- // Calculate new position maintaining the same relative cursor position
584
544
  let new_position = {
585
545
  x: point.x - new_scaled_width * image_percentages.x,
586
546
  y: point.y - new_scaled_height * image_percentages.y
587
547
  };
588
548
 
589
- // Center the image if we're at minimum zoom or if this is a fit zoom operation
590
549
  if (new_zoom === min_zoom || is_fit_zoom) {
591
550
  const canvas_width = this.image_editor_context.app.screen.width;
592
551
  const canvas_height = this.image_editor_context.app.screen.height;
593
552
 
594
- // Center the image, accounting for pad_bottom
595
553
  new_position = {
596
554
  x: (canvas_width - new_scaled_width) / 2,
597
555
  y: (canvas_height - this.pad_bottom - new_scaled_height) / 2
@@ -612,7 +570,6 @@ export class ZoomTool implements Tool {
612
570
  * @private
613
571
  */
614
572
  private handle_pointer_down(event: FederatedPointerEvent): void {
615
- // Only allow dragging if the current tool is 'pan'
616
573
  if (event.button === 0 && this.current_tool === "pan") {
617
574
  this.is_pointer_dragging = true;
618
575
  this.drag_start.copyFrom(event.global);
@@ -627,7 +584,6 @@ export class ZoomTool implements Tool {
627
584
  * @private
628
585
  */
629
586
  private handle_pointer_move(event: FederatedPointerEvent): void {
630
- // Only handle move if we're dragging and the tool is 'pan'
631
587
  if (this.is_pointer_dragging && this.current_tool === "pan") {
632
588
  const raw_position = {
633
589
  x: event.global.x - this.drag_start.x,
@@ -643,11 +599,9 @@ export class ZoomTool implements Tool {
643
599
 
644
600
  let final_position = { ...raw_position };
645
601
 
646
- // If image is smaller than available space, center it
647
602
  if (scaled_width <= available_width) {
648
603
  final_position.x = (canvas.width - scaled_width) / 2;
649
604
  } else {
650
- // Allow free movement during drag, bounds will be applied on pointer up
651
605
  final_position.x = raw_position.x;
652
606
  }
653
607
 
@@ -655,7 +609,6 @@ export class ZoomTool implements Tool {
655
609
  final_position.y =
656
610
  (canvas.height - this.pad_bottom - scaled_height) / 2;
657
611
  } else {
658
- // Allow free movement during drag, bounds will be applied on pointer up
659
612
  final_position.y = raw_position.y;
660
613
  }
661
614
 
@@ -672,7 +625,6 @@ export class ZoomTool implements Tool {
672
625
  * @private
673
626
  */
674
627
  private handle_pointer_up(event: FederatedPointerEvent): void {
675
- // Only handle up if we're dragging and the tool is 'pan'
676
628
  if (this.is_pointer_dragging && this.current_tool === "pan") {
677
629
  this.is_pointer_dragging = false;
678
630
 
@@ -690,11 +642,9 @@ export class ZoomTool implements Tool {
690
642
 
691
643
  let final_position = { ...raw_position };
692
644
 
693
- // If image is smaller than available space, center it
694
645
  if (scaled_width <= available_width) {
695
646
  final_position.x = (canvas.width - scaled_width) / 2;
696
647
  } else {
697
- // Apply 50% rule for horizontal snapping
698
648
  const max_offset = canvas.width / 2;
699
649
  const left_bound = max_offset;
700
650
  const right_bound = canvas.width - max_offset - scaled_width;
@@ -708,7 +658,6 @@ export class ZoomTool implements Tool {
708
658
  final_position.y =
709
659
  (canvas.height - this.pad_bottom - scaled_height) / 2;
710
660
  } else {
711
- // Apply 50% rule for vertical snapping, accounting for pad_bottom
712
661
  const max_offset = (canvas.height - this.pad_bottom) / 2;
713
662
  const top_bound = max_offset;
714
663
  const bottom_bound =
@@ -1,73 +0,0 @@
1
- import { writable } from "svelte/store";
2
- /**
3
- * Creates a command node
4
- * @param command command to add to the node
5
- * @returns a command node
6
- */
7
- function command_node(command) {
8
- return {
9
- command: command || null,
10
- next: null,
11
- previous: null,
12
- push: function (command) {
13
- const node = command_node(command);
14
- node.previous = this;
15
- this.next = node;
16
- }
17
- };
18
- }
19
- /**
20
- * Creates a command manager
21
- * @returns a command manager
22
- */
23
- export function command_manager() {
24
- let history = command_node();
25
- const can_undo = writable(false);
26
- const can_redo = writable(false);
27
- const current_history = writable(history);
28
- return {
29
- undo: function () {
30
- if (history.previous) {
31
- history.command?.undo();
32
- history = history.previous;
33
- }
34
- can_undo.set(!!history.previous);
35
- can_redo.set(!!history.next);
36
- current_history.set(history);
37
- },
38
- redo: function () {
39
- if (history.next) {
40
- history.next.command?.execute();
41
- history = history.next;
42
- }
43
- can_undo.set(!!history.previous);
44
- can_redo.set(!!history.next);
45
- current_history.set(history);
46
- },
47
- execute: function (command) {
48
- command.execute();
49
- history.push(command);
50
- history = history.next;
51
- can_undo.set(!!history.previous);
52
- can_redo.set(!!history.next);
53
- current_history.set(history);
54
- },
55
- hydrate: function (full_history) {
56
- setTimeout(() => {
57
- while (full_history.next) {
58
- this.execute(full_history.next.command);
59
- full_history = full_history.next;
60
- }
61
- }, 1000);
62
- },
63
- can_undo,
64
- can_redo,
65
- current_history,
66
- reset: function () {
67
- history = command_node();
68
- can_undo.set(false);
69
- can_redo.set(false);
70
- current_history.set(history);
71
- }
72
- };
73
- }
@@ -1,174 +0,0 @@
1
- import { writable, type Writable } from "svelte/store";
2
-
3
- /**
4
- * Base command interface that is added to the command_managers history
5
- */
6
- export interface Command {
7
- /**
8
- * Optionally called when the command is first executed for multi-step commands
9
- * @param args arguments to pass to the command
10
- */
11
- start?: (...args: any) => any | Promise<any>;
12
- /**
13
- * Optionally called when the command is continued for multi-step commands
14
- * @param args arguments to pass to the command
15
- */
16
- continue?: (...args: any) => any | Promise<any>;
17
- /**
18
- * Optionally called when the command is stopped for multi-step commands
19
- * @param args arguments to pass to the command
20
- */
21
- stop?: (...args: any) => any | Promise<any>;
22
- /**
23
- * Called by the command manager to execute the command, can act as a no-op if the work has already been done
24
- * This function must be able to recreate the command if the command is undone and redone (`stop`/`start`/`continue` will not be called again)
25
- */
26
- execute(): any | Promise<any>;
27
- /**
28
- * Called by the command manager to undo the command
29
- * This function must be able to undo the work done by the execute function
30
- */
31
- undo(): any | Promise<any>;
32
- }
33
-
34
- /**
35
- * Command manager interface that handles the undo/redo history
36
- */
37
- export interface CommandManager {
38
- /**
39
- * Undo the last command
40
- */
41
- undo(): void;
42
- /**
43
- * Redo the last undone command
44
- */
45
- redo(): void;
46
- /**
47
- * Execute a command and add it to the history
48
- * @param command command to execute
49
- */
50
- execute(command: Command): void;
51
- /**
52
- * Whether or not there are commands that can be undone
53
- * Observable store that you can subscribe to for updates
54
- */
55
- readonly can_undo: Writable<boolean>;
56
- /**
57
- * Whether or not there are commands that can be redone
58
- * Observable store that you can subscribe to for updates
59
- */
60
- readonly can_redo: Writable<boolean>;
61
- /**
62
- * Resets the history
63
- */
64
- reset(): void;
65
- /**
66
- * The current history node
67
- * Observable store that you can subscribe to for updates
68
- */
69
- readonly current_history: Writable<CommandNode>;
70
- /**
71
- * Hydrates the command manager with a full history
72
- * @param full_history the full history to hydrate with
73
- */
74
- hydrate(full_history: CommandNode): void;
75
- }
76
-
77
- /**
78
- * Command node interface that is used to create the undo/redo history
79
- */
80
- export interface CommandNode {
81
- /**
82
- * Command that the node holds
83
- */
84
- command: Command | null;
85
- /**
86
- * Next command in the history
87
- */
88
- next: CommandNode | null;
89
- /**
90
- * Previous command in the history
91
- */
92
- previous: CommandNode | null;
93
- /**
94
- * Push a command onto the history
95
- * @param command command to push onto the history
96
- */
97
- push(command: Command): void;
98
- }
99
-
100
- /**
101
- * Creates a command node
102
- * @param command command to add to the node
103
- * @returns a command node
104
- */
105
- function command_node(command?: Command): CommandNode {
106
- return {
107
- command: command || null,
108
- next: null,
109
- previous: null,
110
- push: function (command: Command) {
111
- const node = command_node(command);
112
- node.previous = this;
113
- this.next = node;
114
- }
115
- };
116
- }
117
-
118
- /**
119
- * Creates a command manager
120
- * @returns a command manager
121
- */
122
- export function command_manager(): CommandManager {
123
- let history: CommandNode = command_node();
124
- const can_undo = writable(false);
125
- const can_redo = writable(false);
126
- const current_history = writable(history);
127
- return {
128
- undo: function () {
129
- if (history.previous) {
130
- history.command?.undo();
131
- history = history.previous;
132
- }
133
- can_undo.set(!!history.previous);
134
- can_redo.set(!!history.next);
135
- current_history.set(history);
136
- },
137
- redo: function () {
138
- if (history.next) {
139
- history.next.command?.execute();
140
- history = history.next;
141
- }
142
-
143
- can_undo.set(!!history.previous);
144
- can_redo.set(!!history.next);
145
- current_history.set(history);
146
- },
147
- execute: function (command: Command) {
148
- command.execute();
149
- history.push(command);
150
- history = history.next!;
151
- can_undo.set(!!history.previous);
152
- can_redo.set(!!history.next);
153
- current_history.set(history);
154
- },
155
- hydrate: function (full_history: CommandNode) {
156
- setTimeout(() => {
157
- while (full_history.next) {
158
- this.execute(full_history.next.command!);
159
- full_history = full_history.next;
160
- }
161
- }, 1000);
162
- },
163
- can_undo,
164
- can_redo,
165
- current_history,
166
-
167
- reset: function () {
168
- history = command_node();
169
- can_undo.set(false);
170
- can_redo.set(false);
171
- current_history.set(history);
172
- }
173
- };
174
- }