@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
@@ -3,16 +3,92 @@ import {
3
3
  RenderTexture,
4
4
  Sprite,
5
5
  Graphics,
6
- Application
6
+ Application,
7
+ Texture
7
8
  } from "pixi.js";
8
9
  import { type ImageEditorContext } from "../core/editor";
10
+ import { type Command } from "../core/commands";
11
+ export class BrushCommand implements Command {
12
+ private layer_id: string;
13
+ private original_texture: Texture;
14
+ private final_texture: Texture;
15
+ name: string;
16
+
17
+ constructor(
18
+ private context: ImageEditorContext,
19
+ layer_id: string,
20
+ original_texture: Texture,
21
+ final_texture: Texture
22
+ ) {
23
+ this.name = "Draw";
24
+ this.layer_id = layer_id;
25
+
26
+ this.original_texture = this.createTextureFrom(original_texture);
27
+ this.final_texture = this.createTextureFrom(final_texture);
28
+ }
29
+
30
+ /**
31
+ * Creates a new texture with the same content as the source texture
32
+ */
33
+ private createTextureFrom(source: Texture): Texture {
34
+ const texture = RenderTexture.create({
35
+ width: source.width,
36
+ height: source.height,
37
+ resolution: window.devicePixelRatio || 1
38
+ });
39
+
40
+ const sprite = new Sprite(source);
41
+ const container = new Container();
42
+ container.addChild(sprite);
43
+
44
+ this.context.app.renderer.render(container, {
45
+ renderTexture: texture
46
+ });
47
+
48
+ container.destroy({ children: true });
49
+ return texture;
50
+ }
51
+
52
+ async execute(): Promise<void> {
53
+ const layer_textures = this.context.layer_manager.get_layer_textures(
54
+ this.layer_id
55
+ );
56
+ if (!layer_textures) return;
57
+
58
+ const temp_sprite = new Sprite(this.final_texture);
59
+ const temp_container = new Container();
60
+ temp_container.addChild(temp_sprite);
61
+
62
+ this.context.app.renderer.render(temp_container, {
63
+ renderTexture: layer_textures.draw
64
+ });
65
+
66
+ temp_container.destroy({ children: true });
67
+ }
68
+
69
+ async undo(): Promise<void> {
70
+ const layer_textures = this.context.layer_manager.get_layer_textures(
71
+ this.layer_id
72
+ );
73
+ if (!layer_textures) return;
74
+
75
+ const temp_sprite = new Sprite(this.original_texture);
76
+ const temp_container = new Container();
77
+ temp_container.addChild(temp_sprite);
78
+
79
+ this.context.app.renderer.render(temp_container, {
80
+ renderTexture: layer_textures.draw
81
+ });
82
+
83
+ temp_container.destroy({ children: true });
84
+ }
85
+ }
9
86
 
10
87
  /**
11
88
  * Class to handle texture operations for the brush tool.
12
89
  * Simplified to work directly with layer textures.
13
90
  */
14
91
  export class BrushTextures {
15
- // Only keep what we need
16
92
  private stroke_texture: RenderTexture | null = null;
17
93
  private erase_texture: RenderTexture | null = null;
18
94
  private display_container: Container | null = null;
@@ -22,15 +98,15 @@ export class BrushTextures {
22
98
  private erase_graphics: Graphics | null = null;
23
99
  private dimensions: { width: number; height: number };
24
100
 
25
- // Track if a new stroke is starting
26
101
  private is_new_stroke = true;
27
102
 
28
- // Track the current drawing opacity
29
103
  private current_opacity = 1.0;
30
104
 
31
- // Track current drawing mode
32
105
  private current_mode: "draw" | "erase" = "draw";
33
106
 
107
+ private original_layer_texture: Texture | null = null;
108
+ private active_layer_id: string | null = null;
109
+
34
110
  constructor(
35
111
  private image_editor_context: ImageEditorContext,
36
112
  private app: Application
@@ -45,10 +121,8 @@ export class BrushTextures {
45
121
  * Initializes textures needed for the brush tool.
46
122
  */
47
123
  initialize_textures(): void {
48
- // Clean up any existing resources
49
124
  this.cleanup_textures();
50
125
 
51
- // Get dimensions from the image container
52
126
  const local_bounds =
53
127
  this.image_editor_context.image_container.getLocalBounds();
54
128
 
@@ -57,24 +131,20 @@ export class BrushTextures {
57
131
  height: local_bounds.height
58
132
  };
59
133
 
60
- // Create a texture for the current stroke preview
61
134
  this.stroke_texture = RenderTexture.create({
62
135
  width: this.dimensions.width,
63
136
  height: this.dimensions.height,
64
137
  resolution: window.devicePixelRatio || 1
65
138
  });
66
139
 
67
- // Create a texture for erasing
68
140
  this.erase_texture = RenderTexture.create({
69
141
  width: this.dimensions.width,
70
142
  height: this.dimensions.height,
71
143
  resolution: window.devicePixelRatio || 1
72
144
  });
73
145
 
74
- // Create a container to display preview
75
146
  this.display_container = new Container();
76
147
 
77
- // Get the active layer and add the display container to it
78
148
  const active_layer =
79
149
  this.image_editor_context.layer_manager.get_active_layer();
80
150
  if (active_layer) {
@@ -85,21 +155,16 @@ export class BrushTextures {
85
155
  );
86
156
  }
87
157
 
88
- // Create container for the stroke graphics
89
158
  this.stroke_container = new Container();
90
159
  this.stroke_graphics = new Graphics();
91
160
  this.stroke_container.addChild(this.stroke_graphics);
92
161
 
93
- // Create graphics for eraser mask
94
162
  this.erase_graphics = new Graphics();
95
163
 
96
- // Create a sprite for the preview of the current stroke
97
164
  this.preview_sprite = new Sprite(this.stroke_texture);
98
- // Start with the preview sprite invisible
99
165
  this.preview_sprite.alpha = 0;
100
166
  this.display_container.addChild(this.preview_sprite);
101
167
 
102
- // Clear the stroke texture
103
168
  const clear_container = new Container();
104
169
  this.app.renderer.render(clear_container, {
105
170
  renderTexture: this.stroke_texture
@@ -109,7 +174,6 @@ export class BrushTextures {
109
174
  });
110
175
  clear_container.destroy();
111
176
 
112
- // Reset drawing state
113
177
  this.is_new_stroke = true;
114
178
  this.current_opacity = 1.0;
115
179
  }
@@ -132,19 +196,16 @@ export class BrushTextures {
132
196
  * Cleans up texture resources.
133
197
  */
134
198
  cleanup_textures(): void {
135
- // Clean up stroke texture
136
199
  if (this.stroke_texture) {
137
200
  this.stroke_texture.destroy();
138
201
  this.stroke_texture = null;
139
202
  }
140
203
 
141
- // Clean up erase texture
142
204
  if (this.erase_texture) {
143
205
  this.erase_texture.destroy();
144
206
  this.erase_texture = null;
145
207
  }
146
208
 
147
- // Clean up display container
148
209
  if (this.display_container) {
149
210
  if (this.display_container.parent) {
150
211
  this.display_container.parent.removeChild(this.display_container);
@@ -153,18 +214,56 @@ export class BrushTextures {
153
214
  this.display_container = null;
154
215
  }
155
216
 
156
- // Nullify other references
217
+ if (this.original_layer_texture) {
218
+ this.original_layer_texture.destroy();
219
+ this.original_layer_texture = null;
220
+ }
221
+
157
222
  this.stroke_container = null;
158
223
  this.stroke_graphics = null;
159
224
  this.preview_sprite = null;
160
225
  this.erase_graphics = null;
226
+ this.active_layer_id = null;
161
227
  }
162
228
 
163
229
  /**
164
230
  * Preserve the canvas state when starting a new stroke.
165
231
  */
166
232
  preserve_canvas_state(): void {
167
- // Reset for a new stroke
233
+ const active_layer =
234
+ this.image_editor_context.layer_manager.get_active_layer();
235
+ if (!active_layer) return;
236
+
237
+ const layers = this.image_editor_context.layer_manager.get_layers();
238
+ const layer = layers.find((l) => l.container === active_layer);
239
+ if (!layer) return;
240
+
241
+ this.active_layer_id = layer.id;
242
+
243
+ const layer_textures =
244
+ this.image_editor_context.layer_manager.get_layer_textures(layer.id);
245
+ if (!layer_textures) return;
246
+
247
+ if (this.original_layer_texture) {
248
+ this.original_layer_texture.destroy();
249
+ }
250
+
251
+ this.original_layer_texture = RenderTexture.create({
252
+ width: this.dimensions.width,
253
+ height: this.dimensions.height,
254
+ resolution: window.devicePixelRatio || 1
255
+ });
256
+
257
+ const temp_sprite = new Sprite(layer_textures.draw);
258
+ const temp_container = new Container();
259
+ temp_container.addChild(temp_sprite);
260
+
261
+ this.app.renderer.render(temp_container, {
262
+ renderTexture: this.original_layer_texture
263
+ });
264
+
265
+ temp_container.destroy({ children: true });
266
+
168
267
  this.is_new_stroke = true;
169
268
  }
170
269
 
@@ -174,7 +273,6 @@ export class BrushTextures {
174
273
  reset_eraser_mask(): void {
175
274
  if (!this.erase_graphics || !this.erase_texture) return;
176
275
 
177
- // Clear and reset the eraser graphics
178
276
  this.erase_graphics.clear();
179
277
  this.erase_graphics.beginFill(0xffffff, 1);
180
278
  this.erase_graphics.drawRect(
@@ -185,90 +283,100 @@ export class BrushTextures {
185
283
  );
186
284
  this.erase_graphics.endFill();
187
285
 
188
- // Render the eraser graphics to the erase texture
189
286
  this.app.renderer.render(this.erase_graphics, {
190
287
  renderTexture: this.erase_texture
191
288
  });
192
289
  }
193
290
 
194
291
  /**
195
- * Commits the current stroke to the active layer.
292
+ * Commits the current stroke to the active layer and returns a command for undo/redo.
196
293
  */
197
294
  commit_stroke(): void {
198
- if (!this.stroke_texture || !this.preview_sprite || !this.stroke_graphics)
295
+ if (
296
+ !this.stroke_texture ||
297
+ !this.preview_sprite ||
298
+ !this.stroke_graphics ||
299
+ !this.original_layer_texture ||
300
+ !this.active_layer_id
301
+ )
199
302
  return;
200
303
 
201
- // Hide the preview sprite
202
304
  this.preview_sprite.visible = false;
203
305
 
204
- // Get the active layer's texture
205
306
  const active_layer =
206
307
  this.image_editor_context.layer_manager.get_active_layer();
207
308
 
208
309
  if (!active_layer) return;
209
310
 
210
- // Get all layers to find the ID of the active layer
211
311
  const layers = this.image_editor_context.layer_manager.get_layers();
212
312
  const layer = layers.find((l) => l.container === active_layer);
213
313
  if (!layer) return;
214
314
 
215
- // Get the active layer's draw texture
315
+ if (layer.id !== this.active_layer_id) return;
316
+
216
317
  const layer_textures =
217
318
  this.image_editor_context.layer_manager.get_layer_textures(layer.id);
218
319
  if (!layer_textures) return;
219
320
 
220
- // Save the current opacity for use in committing
221
321
  const currentOpacity = this.current_opacity;
222
322
 
223
- // Handle drawing or erasing
224
323
  if (this.current_mode === "draw") {
225
- // Drawing mode - simple composition
226
324
  const temp_container = new Container();
227
325
 
228
- // Add current layer texture as base
229
326
  const base_sprite = new Sprite(layer_textures.draw);
230
327
  temp_container.addChild(base_sprite);
231
328
 
232
- // Add the current stroke with proper opacity
233
329
  const stroke_sprite = new Sprite(this.stroke_texture);
234
330
  stroke_sprite.alpha = currentOpacity;
235
331
  temp_container.addChild(stroke_sprite);
236
332
 
237
- // Render the composited result directly to the layer texture
238
333
  this.app.renderer.render(temp_container, {
239
334
  renderTexture: layer_textures.draw
240
335
  });
241
336
 
242
- // Clean up
243
337
  temp_container.destroy({ children: true });
244
338
  } else {
245
- // Erasing mode - use proper masking with setMask
246
339
  if (!this.stroke_texture) return;
247
340
 
248
- // Create a container for the erase operation
249
341
  const erase_container = new Container();
250
342
 
251
- // Add the current layer content
252
343
  const content_sprite = new Sprite(layer_textures.draw);
253
344
  erase_container.addChild(content_sprite);
254
345
 
255
- // Create the mask sprite from our stroke texture
256
346
  const mask_sprite = new Sprite(this.stroke_texture);
257
347
 
258
- // Apply the mask to the container using setMask with inverse: true
259
- // This will only show content where the mask is NOT (inverse: true)
260
348
  erase_container.setMask({ mask: mask_sprite, inverse: true });
261
349
 
262
- // Render the masked content back to layer texture
263
350
  this.app.renderer.render(erase_container, {
264
351
  renderTexture: layer_textures.draw
265
352
  });
266
353
 
267
- // Clean up
268
354
  erase_container.destroy({ children: true });
269
355
  }
270
356
 
271
- // Clear the stroke graphics and texture for the next stroke
357
+ const final_texture = RenderTexture.create({
358
+ width: this.dimensions.width,
359
+ height: this.dimensions.height,
360
+ resolution: window.devicePixelRatio || 1
361
+ });
362
+
363
+ const final_container = new Container();
364
+ const final_sprite = new Sprite(layer_textures.draw);
365
+ final_container.addChild(final_sprite);
366
+
367
+ this.app.renderer.render(final_container, {
368
+ renderTexture: final_texture
369
+ });
370
+
371
+ final_container.destroy({ children: true });
372
+
373
+ const brush_command = new BrushCommand(
374
+ this.image_editor_context,
375
+ this.active_layer_id,
376
+ this.original_layer_texture,
377
+ final_texture
378
+ );
379
+
272
380
  if (this.stroke_graphics) {
273
381
  this.stroke_graphics.clear();
274
382
  }
@@ -278,8 +386,14 @@ export class BrushTextures {
278
386
  });
279
387
  clear_container.destroy();
280
388
 
281
- // Reset state for the next stroke
282
389
  this.is_new_stroke = true;
390
+
391
+ final_texture.destroy();
392
+
393
+ this.original_layer_texture = null;
394
+ this.active_layer_id = null;
395
+
396
+ this.image_editor_context.command_manager.execute(brush_command);
283
397
  }
284
398
 
285
399
  /**
@@ -315,20 +429,19 @@ export class BrushTextures {
315
429
  )
316
430
  return;
317
431
 
318
- // Store the current mode and opacity
432
+ if (this.is_new_stroke && !this.original_layer_texture) {
433
+ this.preserve_canvas_state();
434
+ }
435
+
319
436
  this.current_mode = mode;
320
437
  this.current_opacity =
321
438
  mode === "draw" ? Math.min(Math.max(opacity, 0), 1) : 0.5;
322
439
 
323
- // Apply scale to brush size for consistent rendering
324
440
  const scaled_size = size;
325
441
 
326
- // For new strokes, clear the graphics and texture completely
327
442
  if (this.is_new_stroke) {
328
- // Clean up for a new stroke
329
443
  this.stroke_graphics.clear();
330
444
 
331
- // Clear the stroke texture
332
445
  const clear_container = new Container();
333
446
  this.app.renderer.render(clear_container, {
334
447
  renderTexture: this.stroke_texture
@@ -338,17 +451,13 @@ export class BrushTextures {
338
451
  this.is_new_stroke = false;
339
452
  }
340
453
 
341
- // Set fill color and opacity for drawing
342
454
  if (mode === "draw") {
343
- // For drawing, use the specified color, but always full opacity in the graphics
344
- // We need to convert the color string to a number
345
- let colorValue = 0xffffff; // Default to white
455
+ let colorValue = 0xffffff;
346
456
  try {
347
457
  if (color.startsWith("#")) {
348
458
  colorValue = parseInt(color.replace("#", "0x"), 16);
349
459
  }
350
460
  } catch (e) {
351
- // Default to white if there's an error
352
461
  colorValue = 0xffffff;
353
462
  }
354
463
  this.stroke_graphics.setFillStyle({
@@ -356,57 +465,41 @@ export class BrushTextures {
356
465
  alpha: 1.0
357
466
  });
358
467
  } else {
359
- // For erasing, always use white
360
468
  this.stroke_graphics.setFillStyle({
361
469
  color: 0xffffff,
362
470
  alpha: 1.0
363
471
  });
364
472
  }
365
473
 
366
- // Calculate the distance between the points
367
474
  const distance = this.calculateDistance(from_x, from_y, to_x, to_y);
368
475
 
369
- // If the points are the same, just draw a single circle
370
476
  if (distance < 0.1) {
371
- // Draw a circle at the point - use scaled_size as RADIUS
372
477
  this.stroke_graphics.circle(from_x, from_y, scaled_size).fill();
373
478
  } else {
374
- // Calculate how many circles to draw along the path
375
- // Adaptive spacing that balances quality and performance
376
479
  const spacing = Math.max(scaled_size / 3, 2);
377
480
  const steps = Math.max(Math.ceil(distance / spacing), 2);
378
481
 
379
- // Draw a circle at each point along the path
380
482
  for (let i = 0; i < steps; i++) {
381
483
  const t = i / (steps - 1);
382
484
  const x = from_x + (to_x - from_x) * t;
383
485
  const y = from_y + (to_y - from_y) * t;
384
486
 
385
- // Create a circle at this point - use scaled_size as RADIUS
386
487
  this.stroke_graphics.circle(x, y, scaled_size).fill();
387
488
  }
388
489
  }
389
490
 
390
- // End the fill after all circles are drawn
391
491
  this.stroke_graphics.endFill();
392
492
 
393
- // Render the stroke to the texture
394
493
  this.app.renderer.render(this.stroke_container, {
395
494
  renderTexture: this.stroke_texture
396
495
  });
397
496
 
398
- // Update the preview based on mode
399
497
  if (mode === "draw") {
400
- // For drawing, show the stroke directly with proper opacity
401
498
  this.preview_sprite.texture = this.stroke_texture;
402
499
  this.preview_sprite.alpha = this.current_opacity;
403
500
 
404
- // Set tint to white (no tint) instead of trying to tint the texture
405
501
  this.preview_sprite.tint = 0xffffff;
406
502
  } else {
407
- // For erasing, create a preview that shows what will be erased
408
-
409
- // Get the active layer's texture for preview
410
503
  const active_layer =
411
504
  this.image_editor_context.layer_manager.get_active_layer();
412
505
  if (!active_layer) return;
@@ -419,14 +512,12 @@ export class BrushTextures {
419
512
  this.image_editor_context.layer_manager.get_layer_textures(layer.id);
420
513
  if (!layer_textures) return;
421
514
 
422
- // Create a temporary texture for the preview
423
515
  const preview_texture = RenderTexture.create({
424
516
  width: this.dimensions.width,
425
517
  height: this.dimensions.height,
426
518
  resolution: window.devicePixelRatio || 1
427
519
  });
428
520
 
429
- // First, create a copy of the current layer content
430
521
  const base_container = new Container();
431
522
  const content_sprite = new Sprite(layer_textures.draw);
432
523
  base_container.addChild(content_sprite);
@@ -435,33 +526,25 @@ export class BrushTextures {
435
526
  });
436
527
  base_container.destroy({ children: true });
437
528
 
438
- // Create a container for the eraser preview with red overlay
439
529
  const preview_container = new Container();
440
530
 
441
- // Add our current stroke as the content
442
531
  const mask_sprite = new Sprite(this.stroke_texture);
443
532
 
444
- // Add a red overlay to show what's being erased
445
533
  const overlay = new Graphics();
446
534
  overlay.setFillStyle({ color: 0xffffff, alpha: 0.5 });
447
535
  overlay.rect(0, 0, this.dimensions.width, this.dimensions.height).fill();
448
536
 
449
- // Use setMask with inverse: false to only show overlay where the stroke is
450
- // We want to show the red overlay ONLY where the stroke will erase
451
537
  overlay.setMask({ mask: mask_sprite, inverse: false });
452
538
 
453
539
  preview_container.addChild(overlay);
454
540
 
455
- // Render the overlay on top of the base content
456
541
  this.app.renderer.render(preview_container, {
457
542
  renderTexture: preview_texture
458
543
  });
459
544
 
460
- // Update the preview sprite with our preview texture
461
545
  this.preview_sprite.texture = preview_texture;
462
546
  this.preview_sprite.alpha = 1.0;
463
547
 
464
- // Clean up
465
548
  preview_container.destroy({ children: true });
466
549
  preview_texture.destroy();
467
550
  }