@gradio/imageeditor 0.12.14 → 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 +14 -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 +10 -9
  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
@@ -0,0 +1,1221 @@
1
+ import { Application, Container, Graphics, Sprite, Filter, RenderTexture, GlProgram, defaultFilterVert, SCALE_MODES, Texture, Assets } from "pixi.js";
2
+ import { DropShadowFilter as BlurFilter } from "pixi-filters/drop-shadow";
3
+ import { ImageTool } from "../image/image";
4
+ import { AddImageCommand } from "../image/image";
5
+ import { ZoomTool } from "../zoom/zoom";
6
+ import { spring } from "svelte/motion";
7
+ import { writable, get } from "svelte/store";
8
+ import { Rectangle } from "pixi.js";
9
+ import {} from "../types";
10
+ import { get_canvas_blob } from "../utils/pixi";
11
+ export class CommandManager {
12
+ undo_stack = [];
13
+ redo_stack = [];
14
+ async execute(command) {
15
+ await command.execute();
16
+ this.undo_stack.push(command);
17
+ this.redo_stack = []; // Clear redo stack when new command is executed
18
+ }
19
+ async undo() {
20
+ const command = this.undo_stack.pop();
21
+ if (command) {
22
+ await command.undo();
23
+ this.redo_stack.push(command);
24
+ }
25
+ }
26
+ async redo() {
27
+ const command = this.redo_stack.pop();
28
+ if (command) {
29
+ await command.execute();
30
+ this.undo_stack.push(command);
31
+ }
32
+ }
33
+ }
34
+ export class LayerManager {
35
+ layers = [];
36
+ active_layer = null;
37
+ draw_textures = new Map();
38
+ layer_store = writable({
39
+ active_layer: "",
40
+ layers: []
41
+ });
42
+ background_layer = null;
43
+ image_container;
44
+ app;
45
+ fixed_canvas;
46
+ dark;
47
+ border_region;
48
+ layer_options;
49
+ constructor(image_container, app, fixed_canvas, dark, border_region, layer_options) {
50
+ this.image_container = image_container;
51
+ this.app = app;
52
+ this.fixed_canvas = fixed_canvas;
53
+ this.dark = dark;
54
+ this.border_region = border_region;
55
+ this.layer_options = layer_options;
56
+ }
57
+ create_background_layer(width, height) {
58
+ if (this.background_layer) {
59
+ this.background_layer.destroy();
60
+ }
61
+ // Create new background layer
62
+ const layer = new Container();
63
+ this.background_layer = layer;
64
+ // Create a render texture for the background with transparency
65
+ const bg_texture = RenderTexture.create({
66
+ width,
67
+ height,
68
+ resolution: window.devicePixelRatio,
69
+ antialias: true,
70
+ scaleMode: SCALE_MODES.NEAREST
71
+ });
72
+ // Create sprite to display the texture with transparency
73
+ const bg_sprite = new Sprite(bg_texture);
74
+ layer.addChild(bg_sprite);
75
+ // Clear the texture with transparency
76
+ const clear_graphics = new Graphics();
77
+ clear_graphics.clear();
78
+ clear_graphics
79
+ .rect(0, 0, width, height)
80
+ .fill({ color: this.dark ? 0x333333 : 0xffffff, alpha: 1 });
81
+ // Render with transparency
82
+ this.app.renderer.render({
83
+ container: clear_graphics,
84
+ target: bg_texture,
85
+ clear: true
86
+ });
87
+ this.image_container.addChild(layer);
88
+ layer.zIndex = -1;
89
+ this.update_layer_order();
90
+ return layer;
91
+ }
92
+ set_layer_options(layer_options, width, height) {
93
+ this.layer_options = layer_options;
94
+ this.reset_layers(width, height);
95
+ }
96
+ /**
97
+ * Creates a background layer from an image URL
98
+ * @param url The URL of the image to use as background
99
+ * @param width The width of the layer (if fixed_canvas is true)
100
+ * @param height The height of the layer (if fixed_canvas is true)
101
+ * @param borderRegion Minimum border region to add around the image for outpainting (in pixels)
102
+ * @returns A Promise that resolves to the background Container when the image is loaded
103
+ */
104
+ async create_background_layer_from_url(url, width, height) {
105
+ const layer = this.create_background_layer(width || this.image_container.width, height || this.image_container.height);
106
+ try {
107
+ const texture = await Texture.from(url);
108
+ const sprite = new Sprite(texture);
109
+ const imageWidth = sprite.texture.width;
110
+ const imageHeight = sprite.texture.height;
111
+ // Calculate dimensions based on whether canvas size is fixed
112
+ const containerWidth = width || this.image_container.width;
113
+ const containerHeight = height || this.image_container.height;
114
+ if (this.fixed_canvas) {
115
+ // If fixed canvas, fit the image within the specified dimensions minus border region
116
+ // Calculate aspect-preserving dimensions
117
+ const effectiveContainerWidth = Math.max(containerWidth - this.border_region * 2, 10);
118
+ const effectiveContainerHeight = Math.max(containerHeight - this.border_region * 2, 10);
119
+ const imageAspectRatio = imageWidth / imageHeight;
120
+ const containerAspectRatio = effectiveContainerWidth / effectiveContainerHeight;
121
+ let finalWidth, finalHeight;
122
+ let posX = this.border_region, // Start with border offset
123
+ posY = this.border_region;
124
+ if (imageWidth <= effectiveContainerWidth &&
125
+ imageHeight <= effectiveContainerHeight) {
126
+ // Image fits within container without scaling
127
+ finalWidth = imageWidth;
128
+ finalHeight = imageHeight;
129
+ }
130
+ else {
131
+ if (imageAspectRatio > containerAspectRatio) {
132
+ // Width is the limiting factor
133
+ finalWidth = effectiveContainerWidth;
134
+ finalHeight = effectiveContainerWidth / imageAspectRatio;
135
+ }
136
+ else {
137
+ // Height is the limiting factor
138
+ finalHeight = effectiveContainerHeight;
139
+ finalWidth = effectiveContainerHeight * imageAspectRatio;
140
+ }
141
+ }
142
+ // Center image within the effective container area (which already accounts for border)
143
+ posX += Math.round((effectiveContainerWidth - finalWidth) / 2);
144
+ posY += Math.round((effectiveContainerHeight - finalHeight) / 2);
145
+ sprite.width = finalWidth;
146
+ sprite.height = finalHeight;
147
+ sprite.position.set(posX, posY);
148
+ }
149
+ else {
150
+ // If not fixed canvas, use the natural dimensions of the image plus border
151
+ // Position starts at the border offset
152
+ sprite.position.set(this.border_region, this.border_region);
153
+ // We need to re-create the background layer with the actual image dimensions plus border
154
+ if (this.background_layer) {
155
+ this.background_layer.destroy();
156
+ }
157
+ // Create new layer with image dimensions plus border on all sides
158
+ const totalWidth = imageWidth + this.border_region * 2;
159
+ const totalHeight = imageHeight + this.border_region * 2;
160
+ const newLayer = this.create_background_layer(totalWidth, totalHeight);
161
+ sprite.width = imageWidth;
162
+ sprite.height = imageHeight;
163
+ newLayer.addChild(sprite);
164
+ return newLayer;
165
+ }
166
+ layer.addChild(sprite);
167
+ return layer;
168
+ }
169
+ catch (error) {
170
+ console.error("Error loading image from URL:", error);
171
+ return layer;
172
+ }
173
+ }
174
+ create_layer({ width, height, layer_name, user_created }) {
175
+ const layer = new Container();
176
+ const layer_id = Math.random().toString(36).substring(2, 15);
177
+ const _layer_name = layer_name || `Layer ${this.layers.length + 1}`;
178
+ this.layers.push({
179
+ name: _layer_name,
180
+ id: layer_id,
181
+ container: layer,
182
+ user_created
183
+ });
184
+ this.image_container.addChild(layer);
185
+ // Create a single render texture for drawing with transparency
186
+ const draw_texture = RenderTexture.create({
187
+ width,
188
+ height,
189
+ resolution: window.devicePixelRatio,
190
+ antialias: true,
191
+ scaleMode: SCALE_MODES.NEAREST
192
+ });
193
+ const canvas_sprite = new Sprite(draw_texture);
194
+ layer.addChild(canvas_sprite);
195
+ const clear_graphics = new Graphics();
196
+ clear_graphics.clear();
197
+ clear_graphics.beginFill(0, 0);
198
+ clear_graphics.drawRect(0, 0, width, height);
199
+ clear_graphics.endFill();
200
+ this.app.renderer.render({
201
+ container: clear_graphics,
202
+ target: draw_texture,
203
+ clear: true
204
+ });
205
+ // Store texture for drawing
206
+ this.draw_textures.set(layer, draw_texture);
207
+ this.update_layer_order();
208
+ this.active_layer = layer;
209
+ this.layer_store.set({
210
+ active_layer: layer_id,
211
+ layers: this.layers
212
+ });
213
+ return layer;
214
+ }
215
+ /**
216
+ * Creates a new layer with an image loaded from a URL
217
+ * @param url The URL of the image to load
218
+ * @param borderRegion Minimum border region to add around the image for outpainting (in pixels)
219
+ * @returns A Promise that resolves to the layer ID
220
+ */
221
+ async add_layer_from_url(url) {
222
+ const { width, height } = this.image_container.getLocalBounds();
223
+ const layer = this.create_layer({
224
+ width,
225
+ height,
226
+ layer_name: "Layer 1",
227
+ user_created: true
228
+ });
229
+ const layerIndex = this.layers.findIndex((l) => l.container === layer);
230
+ if (layerIndex === -1) {
231
+ console.error("Could not find newly created layer");
232
+ return "";
233
+ }
234
+ const layerId = this.layers[layerIndex].id;
235
+ try {
236
+ const texture = await Assets.load(url);
237
+ const drawTexture = this.draw_textures.get(layer);
238
+ if (!drawTexture) {
239
+ console.error("No draw texture found for layer");
240
+ return layerId;
241
+ }
242
+ const sprite = new Sprite(texture);
243
+ const imageWidth = sprite.width;
244
+ const imageHeight = sprite.height;
245
+ let posX = this.border_region;
246
+ let posY = this.border_region;
247
+ // Calculate effective dimensions (accounting for border)
248
+ const effectiveWidth = this.fixed_canvas
249
+ ? width - this.border_region * 2
250
+ : width;
251
+ const effectiveHeight = this.fixed_canvas
252
+ ? height - this.border_region * 2
253
+ : height;
254
+ // If the image is smaller than the effective layer area, center it within that area
255
+ if (imageWidth < effectiveWidth || imageHeight < effectiveHeight) {
256
+ posX = Math.floor((effectiveWidth - imageWidth) / 2);
257
+ posY = Math.floor((effectiveHeight - imageHeight) / 2);
258
+ }
259
+ sprite.position.set(posX, posY);
260
+ // If the image is larger than the effective layer area, scale it down to fit
261
+ if (imageWidth > effectiveWidth || imageHeight > effectiveHeight) {
262
+ const imageAspectRatio = imageWidth / imageHeight;
263
+ const areaAspectRatio = effectiveWidth / effectiveHeight;
264
+ let finalWidth, finalHeight;
265
+ if (imageAspectRatio > areaAspectRatio) {
266
+ // Width is the limiting factor
267
+ finalWidth = effectiveWidth;
268
+ finalHeight = effectiveWidth / imageAspectRatio;
269
+ }
270
+ else {
271
+ // Height is the limiting factor
272
+ finalHeight = effectiveHeight;
273
+ finalWidth = effectiveHeight * imageAspectRatio;
274
+ }
275
+ sprite.width = finalWidth;
276
+ sprite.height = finalHeight;
277
+ // Recalculate position to center within effective area
278
+ posX =
279
+ this.border_region + Math.floor((effectiveWidth - finalWidth) / 2);
280
+ posY =
281
+ this.border_region + Math.floor((effectiveHeight - finalHeight) / 2);
282
+ sprite.position.set(posX, posY);
283
+ }
284
+ this.app.renderer.render(sprite, { renderTexture: drawTexture });
285
+ this.set_active_layer(layerId);
286
+ return layerId;
287
+ }
288
+ catch (error) {
289
+ console.error("Error loading image from URL:", error);
290
+ return layerId;
291
+ }
292
+ }
293
+ get_active_layer() {
294
+ return this.active_layer;
295
+ }
296
+ set_active_layer(id) {
297
+ if (this.layers.some((l) => l.id === id)) {
298
+ this.active_layer =
299
+ this.layers.find((l) => l.id === id)?.container ||
300
+ this.layers[0]?.container ||
301
+ null;
302
+ this.layer_store.set({
303
+ active_layer: id,
304
+ layers: this.layers
305
+ });
306
+ }
307
+ }
308
+ get_layers() {
309
+ return this.layers;
310
+ }
311
+ get_layer_textures(id) {
312
+ const layer = this.layers.find((l) => l.id === id);
313
+ if (layer) {
314
+ const draw = this.draw_textures.get(layer.container);
315
+ if (draw) {
316
+ return { draw };
317
+ }
318
+ }
319
+ return null;
320
+ }
321
+ delete_layer(id) {
322
+ const index = this.layers.findIndex((l) => l.id === id);
323
+ if (index > -1) {
324
+ // Clean up texture
325
+ const draw_texture = this.draw_textures.get(this.layers[index].container);
326
+ if (draw_texture) {
327
+ draw_texture.destroy();
328
+ this.draw_textures.delete(this.layers[index].container);
329
+ }
330
+ this.layers[index].container.destroy();
331
+ if (this.active_layer === this.layers[index].container) {
332
+ this.active_layer =
333
+ this.layers[Math.max(0, index - 1)]?.container || null;
334
+ }
335
+ this.layers = this.layers.filter((l) => l.id !== id);
336
+ this.layer_store.update((_layers) => ({
337
+ active_layer: _layers.active_layer === id
338
+ ? this.layers[this.layers.length - 1]?.id
339
+ : _layers.active_layer,
340
+ layers: this.layers
341
+ }));
342
+ this.update_layer_order();
343
+ }
344
+ }
345
+ update_layer_order() {
346
+ // Ensure background layer always has lowest z-index
347
+ if (this.background_layer) {
348
+ this.background_layer.zIndex = -1;
349
+ }
350
+ // Update other layers starting from 0
351
+ this.layers.forEach((layer, index) => {
352
+ layer.container.zIndex = index;
353
+ });
354
+ }
355
+ move_layer(id, direction) {
356
+ const index = this.layers.findIndex((l) => l.id === id);
357
+ if (index > -1) {
358
+ const new_index = direction === "up" ? index - 1 : index + 1;
359
+ this.layers = this.layers.map((l, i) => {
360
+ if (i === index) {
361
+ return this.layers[new_index];
362
+ }
363
+ if (i === new_index) {
364
+ return this.layers[index];
365
+ }
366
+ return l;
367
+ });
368
+ this.update_layer_order();
369
+ this.layer_store.update((_layers) => ({
370
+ active_layer: id,
371
+ layers: this.layers
372
+ }));
373
+ }
374
+ }
375
+ /**
376
+ * Resizes all layers to a new width and height
377
+ * @param newWidth The new width of the layers
378
+ * @param newHeight The new height of the layers
379
+ * @param scale If true, scales the layer content to fit the new dimensions. If false, keeps the content size unchanged.
380
+ * @param anchor The anchor point to position the content relative to the new size
381
+ */
382
+ resize_all_layers(newWidth, newHeight, scale, anchor,
383
+ // Add old canvas dimensions for anchor calculations when not scaling
384
+ oldCanvasWidth, oldCanvasHeight) {
385
+ // Create a map of the old layers by ID for reference
386
+ const oldLayersById = new Map(this.layers.map((l) => [l.id, l]));
387
+ const oldBackgroundLayer = this.background_layer;
388
+ // Calculate offset for non-scaling resize based on anchor and dimension change
389
+ const calculateOffset = () => {
390
+ let offsetX = 0;
391
+ let offsetY = 0;
392
+ const deltaWidth = newWidth - oldCanvasWidth;
393
+ const deltaHeight = newHeight - oldCanvasHeight;
394
+ // Determine X offset based on anchor point
395
+ if (anchor.includes("left")) {
396
+ offsetX = 0;
397
+ }
398
+ else if (anchor.includes("right")) {
399
+ offsetX = deltaWidth;
400
+ }
401
+ else {
402
+ // center
403
+ offsetX = Math.floor(deltaWidth / 2);
404
+ }
405
+ // Determine Y offset based on anchor point
406
+ if (anchor.includes("top")) {
407
+ offsetY = 0;
408
+ }
409
+ else if (anchor.includes("bottom")) {
410
+ offsetY = deltaHeight;
411
+ }
412
+ else {
413
+ // center
414
+ offsetY = Math.floor(deltaHeight / 2);
415
+ }
416
+ return { offsetX, offsetY };
417
+ };
418
+ // First, handle the background layer using the helper method
419
+ // Note: create_background_layer called inside the helper might destroy the old one
420
+ this.background_layer = this._resize_background_layer(oldBackgroundLayer, // Pass the potentially null old layer
421
+ newWidth, newHeight, scale, calculateOffset);
422
+ // Now process regular layers
423
+ const processedLayers = [];
424
+ // Store old layer data before clearing the main arrays
425
+ const oldLayerData = this.layers.map((oldLayer) => ({
426
+ id: oldLayer.id,
427
+ name: oldLayer.name,
428
+ user_created: oldLayer.user_created,
429
+ texture: this.draw_textures.get(oldLayer.container),
430
+ container: oldLayer.container
431
+ }));
432
+ // Clear the current layers array and textures map. New layers will be added by the helper.
433
+ this.layers = [];
434
+ this.draw_textures.clear(); // Ensure map is cleared before helpers add to it
435
+ // Rebuild layers in the original order using the helper method
436
+ for (const oldData of oldLayerData) {
437
+ // The helper _resize_single_layer handles creating the new layer,
438
+ // drawing content, cleanup, and adding the new layer to this.layers.
439
+ // It returns null if resizing failed for that layer.
440
+ const newLayer = this._resize_single_layer(oldData, newWidth, newHeight, scale, calculateOffset);
441
+ if (newLayer) {
442
+ processedLayers.push(newLayer);
443
+ }
444
+ }
445
+ // Ensure the active layer still exists, otherwise select the top one
446
+ const currentActiveId = get(this.layer_store).active_layer;
447
+ const activeLayerExists = processedLayers.some((l) => l.id === currentActiveId);
448
+ if (!activeLayerExists && processedLayers.length > 0) {
449
+ this.set_active_layer(processedLayers[0].id); // Set to top layer (index 0 is bottom visually)
450
+ }
451
+ else if (processedLayers.length === 0) {
452
+ this.layer_store.update((s) => ({ ...s, active_layer: "" })); // No layers left
453
+ }
454
+ else {
455
+ // If the active layer still exists, just trigger an update to ensure consistency
456
+ this.layer_store.update((s) => ({ ...s }));
457
+ }
458
+ // Update Z-indices
459
+ this.update_layer_order();
460
+ // Final cleanup attempt - textures might need explicit destroy calls
461
+ // Using setTimeout allows the current execution stack to clear
462
+ setTimeout(() => {
463
+ // Attempt to force GC or resource cleanup if needed, though Pixi often handles this
464
+ Assets.cache.reset(); // Be careful with this, might clear too much
465
+ this.app.renderer.textureGC.run(); // Force texture garbage collection
466
+ }, 100);
467
+ }
468
+ /**
469
+ * Helper method to resize the background layer.
470
+ */
471
+ _resize_background_layer(oldBackgroundLayer, newWidth, newHeight, scale, calculateOffset) {
472
+ if (!oldBackgroundLayer) {
473
+ // If no old background layer existed, create a new one
474
+ return this.create_background_layer(newWidth, newHeight);
475
+ }
476
+ // Identify the actual image sprite within the background container
477
+ let backgroundImage = oldBackgroundLayer.children.find((child) => child instanceof Sprite &&
478
+ child.texture !== oldBackgroundLayer.children[0]?.texture);
479
+ // Create the new background layer first (this also destroys the old one implicitly if it exists)
480
+ const newBackgroundLayer = this.create_background_layer(newWidth, newHeight);
481
+ if (backgroundImage) {
482
+ const newBgImage = new Sprite(backgroundImage.texture);
483
+ newBgImage.width = backgroundImage.width; // Preserve original size unless scaling
484
+ newBgImage.height = backgroundImage.height;
485
+ if (scale) {
486
+ newBgImage.width = newWidth;
487
+ newBgImage.height = newHeight;
488
+ newBgImage.position.set(0, 0);
489
+ }
490
+ else {
491
+ const { offsetX, offsetY } = calculateOffset();
492
+ newBgImage.position.set(backgroundImage.x + offsetX, backgroundImage.y + offsetY);
493
+ }
494
+ newBackgroundLayer.addChild(newBgImage);
495
+ }
496
+ // If no background image, the new layer created above is already blank and correctly sized.
497
+ return newBackgroundLayer;
498
+ }
499
+ /**
500
+ * Helper method to resize a single regular layer.
501
+ */
502
+ _resize_single_layer(oldData, newWidth, newHeight, scale, calculateOffset) {
503
+ if (!oldData.texture) {
504
+ console.warn(`No texture found for layer ${oldData.id}, skipping cleanup.`);
505
+ // Destroy the old container if it exists and is not destroyed
506
+ if (oldData.container && !oldData.container.destroyed) {
507
+ if (this.image_container.children.includes(oldData.container)) {
508
+ this.image_container.removeChild(oldData.container);
509
+ }
510
+ oldData.container.destroy({ children: true });
511
+ }
512
+ return null;
513
+ }
514
+ // Create a new layer container and associated render texture
515
+ const newContainer = this.create_layer({
516
+ // This adds the layer to this.layers and draw_textures
517
+ width: newWidth,
518
+ height: newHeight,
519
+ layer_name: oldData.name,
520
+ user_created: oldData.user_created
521
+ });
522
+ // Retrieve the newly created layer data (it's the last one added by create_layer)
523
+ const newLayer = this.layers[this.layers.length - 1];
524
+ newLayer.id = oldData.id; // Restore the original ID
525
+ // Get the render texture for the new layer
526
+ const newTexture = this.draw_textures.get(newContainer);
527
+ if (!newTexture) {
528
+ console.error(`Failed to get texture for newly created layer ${newLayer.id}. Cleaning up.`);
529
+ // Clean up the partially created new layer resources
530
+ if (newContainer && !newContainer.destroyed) {
531
+ if (this.image_container.children.includes(newContainer)) {
532
+ this.image_container.removeChild(newContainer);
533
+ }
534
+ newContainer.destroy({ children: true });
535
+ const idx = this.layers.findIndex((l) => l.container === newContainer);
536
+ if (idx > -1)
537
+ this.layers.splice(idx, 1);
538
+ this.draw_textures.delete(newContainer);
539
+ }
540
+ // Clean up old resources
541
+ if (oldData.texture && !oldData.texture.destroyed)
542
+ oldData.texture.destroy(true);
543
+ if (oldData.container && !oldData.container.destroyed)
544
+ oldData.container.destroy({ children: true });
545
+ return null;
546
+ }
547
+ // Explicitly clear the new render texture (create_layer might already do this, but explicit is safer)
548
+ this.app.renderer.clear({ target: newTexture, clearColor: [0, 0, 0, 0] });
549
+ // Create a sprite with the old texture content
550
+ const sprite = new Sprite(oldData.texture);
551
+ if (scale) {
552
+ sprite.width = newWidth;
553
+ sprite.height = newHeight;
554
+ sprite.position.set(0, 0);
555
+ }
556
+ else {
557
+ const { offsetX, offsetY } = calculateOffset();
558
+ sprite.position.set(offsetX, offsetY);
559
+ }
560
+ // Render the sprite onto the new texture
561
+ this.app.renderer.render(sprite, { renderTexture: newTexture });
562
+ // Clean up temporary sprite and old resources
563
+ sprite.destroy();
564
+ if (oldData.texture && !oldData.texture.destroyed) {
565
+ oldData.texture.destroy(true);
566
+ }
567
+ if (oldData.container && !oldData.container.destroyed) {
568
+ if (this.image_container.children.includes(oldData.container)) {
569
+ this.image_container.removeChild(oldData.container);
570
+ }
571
+ oldData.container.destroy({ children: true });
572
+ }
573
+ // Return the data for the successfully resized layer
574
+ return newLayer;
575
+ }
576
+ async get_blobs(width, height) {
577
+ const blobs = {
578
+ background: await get_canvas_blob(this.app.renderer, this.background_layer, {
579
+ width,
580
+ height,
581
+ x: 0,
582
+ y: 0
583
+ }),
584
+ layers: await Promise.all(this.layers.map(async (layer) => {
585
+ const blob = await get_canvas_blob(this.app.renderer, layer.container, {
586
+ width,
587
+ height,
588
+ x: 0,
589
+ y: 0
590
+ });
591
+ if (blob) {
592
+ return blob;
593
+ }
594
+ return null;
595
+ })),
596
+ composite: await get_canvas_blob(this.app.renderer, this.image_container)
597
+ };
598
+ return blobs;
599
+ }
600
+ reset_layers(width, height, persist = false) {
601
+ const _layers_to_recreate = persist
602
+ ? this.layers.map((layer) => layer.name)
603
+ : this.layer_options.layers;
604
+ this.layers.forEach((layer) => {
605
+ this.delete_layer(layer.id);
606
+ });
607
+ for (const layer of _layers_to_recreate) {
608
+ this.create_layer({
609
+ width,
610
+ height,
611
+ layer_name: layer,
612
+ user_created: this.layer_options.layers.includes(layer) ? false : true
613
+ });
614
+ }
615
+ }
616
+ init_layers(width, height) {
617
+ for (const layer of this.layers) {
618
+ this.delete_layer(layer.id);
619
+ }
620
+ for (const layer of this.layer_options.layers) {
621
+ this.create_layer({
622
+ width,
623
+ height,
624
+ layer_name: layer,
625
+ user_created: false
626
+ });
627
+ }
628
+ this.active_layer = this.layers[0].container;
629
+ this.layer_store.update((_layers) => ({
630
+ active_layer: _layers.layers[0].id,
631
+ layers: this.layers
632
+ }));
633
+ }
634
+ }
635
+ const core_tools = ["image", "zoom"];
636
+ const core_tool_map = {
637
+ image: () => new ImageTool(),
638
+ zoom: () => new ZoomTool()
639
+ };
640
+ const spring_config = {
641
+ stiffness: 0.45,
642
+ damping: 0.8
643
+ };
644
+ class EditorState {
645
+ state;
646
+ scale;
647
+ position;
648
+ subscribers;
649
+ constructor(editor) {
650
+ if (!(editor instanceof ImageEditor)) {
651
+ throw new Error("EditorState must be created by ImageEditor");
652
+ }
653
+ // Store private data
654
+ this.scale = 1.0;
655
+ this.position = { x: 0, y: 0 };
656
+ this.subscribers = new Set();
657
+ // @ts-ignore
658
+ this.state = Object.freeze({
659
+ get position() {
660
+ return { ...this.position };
661
+ },
662
+ get scale() {
663
+ return this.scale;
664
+ },
665
+ get current_tool() {
666
+ return this.current_tool;
667
+ },
668
+ get current_subtool() {
669
+ return this.current_subtool;
670
+ },
671
+ subscribe: this.subscribe.bind(this)
672
+ });
673
+ }
674
+ // Internal methods - only accessible to EditorState class
675
+ _set_position(x, y) {
676
+ const oldPosition = { ...this.position };
677
+ this.position = { x, y };
678
+ this._notify_subscribers("position", oldPosition, this.position);
679
+ }
680
+ _set_scale(scale) {
681
+ const oldScale = this.scale;
682
+ this.scale = scale;
683
+ this._notify_subscribers("scale", oldScale, scale);
684
+ }
685
+ _notify_subscribers(property, oldValue, newValue) {
686
+ this.subscribers.forEach((callback) => {
687
+ callback({
688
+ property,
689
+ oldValue,
690
+ newValue,
691
+ timestamp: Date.now()
692
+ });
693
+ });
694
+ }
695
+ // Public method but only affects notifications
696
+ subscribe(callback) {
697
+ this.subscribers.add(callback);
698
+ return () => this.subscribers.delete(callback);
699
+ }
700
+ }
701
+ export class ImageEditor {
702
+ ready;
703
+ background_image_present = writable(false);
704
+ min_zoom = writable(true);
705
+ app;
706
+ ui_container;
707
+ image_container;
708
+ command_manager;
709
+ layer_manager;
710
+ tools = new Map();
711
+ current_tool;
712
+ current_subtool;
713
+ target_element;
714
+ width;
715
+ height;
716
+ dimensions;
717
+ scale;
718
+ position;
719
+ state;
720
+ scale_value = 1;
721
+ position_value = { x: 0, y: 0 };
722
+ dimensions_value = {
723
+ width: 0,
724
+ height: 0
725
+ };
726
+ layers = writable({
727
+ active_layer: "",
728
+ layers: []
729
+ });
730
+ outline_container;
731
+ outline_graphics;
732
+ background_image;
733
+ ready_resolve;
734
+ event_callbacks = new Map();
735
+ fixed_canvas;
736
+ dark;
737
+ border_region;
738
+ layer_options;
739
+ overlay_container;
740
+ overlay_graphics;
741
+ pad_bottom;
742
+ constructor(options) {
743
+ this.pad_bottom = options.pad_bottom || 0;
744
+ this.dark = options.dark || false;
745
+ this.target_element = options.target_element;
746
+ this.width = options.width;
747
+ this.height = options.height;
748
+ this.command_manager = new CommandManager();
749
+ // this.layers = writable([]);
750
+ this.ready = new Promise((resolve) => {
751
+ this.ready_resolve = resolve;
752
+ });
753
+ this.fixed_canvas = options.fixed_canvas || false;
754
+ this.tools = new Map(options.tools.map((tool) => {
755
+ if (typeof tool === "string") {
756
+ return [tool, core_tool_map[tool]()];
757
+ }
758
+ return [tool.name, tool];
759
+ }));
760
+ for (const tool of this.tools.values()) {
761
+ if (tool?.on) {
762
+ tool.on("change", () => {
763
+ this.notify("change");
764
+ });
765
+ }
766
+ }
767
+ this.dimensions = spring({ width: this.width, height: this.height }, spring_config);
768
+ this.scale = spring(1, spring_config);
769
+ this.position = spring({ x: 0, y: 0 }, spring_config);
770
+ this.state = new EditorState(this);
771
+ this.border_region = options.border_region || 0;
772
+ this.layer_options = options.layer_options || {
773
+ allow_additional_layers: true,
774
+ layers: ["Layer 1"],
775
+ disabled: false
776
+ };
777
+ this.scale.subscribe((scale) => {
778
+ this.state._set_scale(scale);
779
+ });
780
+ this.position.subscribe((position) => {
781
+ this.state._set_position(position.x, position.y);
782
+ });
783
+ this.init();
784
+ }
785
+ get context() {
786
+ const editor = this;
787
+ return {
788
+ app: this.app,
789
+ ui_container: this.ui_container,
790
+ image_container: this.image_container,
791
+ get background_image() {
792
+ return editor.background_image;
793
+ },
794
+ pad_bottom: this.pad_bottom,
795
+ command_manager: this.command_manager,
796
+ layer_manager: this.layer_manager,
797
+ dimensions: { subscribe: this.dimensions.subscribe },
798
+ scale: { subscribe: this.scale.subscribe },
799
+ position: { subscribe: this.position.subscribe },
800
+ set_image_properties: this.set_image_properties.bind(this),
801
+ execute_command: this.execute_command.bind(this),
802
+ resize_canvas: this.resize_canvas.bind(this),
803
+ reset: this.reset.bind(this),
804
+ set_background_image: this.set_background_image.bind(this)
805
+ };
806
+ }
807
+ async init() {
808
+ const container_box = this.target_element.getBoundingClientRect();
809
+ new ResizeObserver((entries) => {
810
+ entries.forEach((entry) => {
811
+ this.resize_canvas(entry.contentBoxSize[0].inlineSize, entry.contentBoxSize[0].blockSize);
812
+ });
813
+ }).observe(this.target_element);
814
+ this.app = new Application();
815
+ await this.app.init({
816
+ width: container_box.width,
817
+ height: container_box.height,
818
+ backgroundAlpha: this.dark ? 0 : 1,
819
+ backgroundColor: this.dark ? 0x333333 : 0xffffff,
820
+ resolution: window.devicePixelRatio,
821
+ autoDensity: true,
822
+ antialias: true,
823
+ powerPreference: "high-performance"
824
+ });
825
+ // Set canvas properties
826
+ const canvas = this.app.canvas;
827
+ canvas.style.background = "transparent";
828
+ await this.setup_containers();
829
+ //create background layer
830
+ this.layer_manager.create_background_layer(this.width, this.height);
831
+ // Create initial layer
832
+ this.layer_manager.init_layers(this.width, this.height);
833
+ for (const tool of this.tools.values()) {
834
+ await tool.setup(this.context, this.current_tool, this.current_subtool);
835
+ }
836
+ const zoom = this.tools.get("zoom");
837
+ if (zoom) {
838
+ zoom.min_zoom.subscribe((is_min_zoom) => {
839
+ this.min_zoom.set(is_min_zoom);
840
+ });
841
+ }
842
+ this.target_element.appendChild(canvas);
843
+ // Ensure parent element is transparent
844
+ this.target_element.style.background = "transparent";
845
+ this.dimensions.subscribe((dimensions) => {
846
+ this.dimensions_value = dimensions;
847
+ this.image_container.width = dimensions.width;
848
+ this.image_container.height = dimensions.height;
849
+ });
850
+ this.scale.subscribe((scale) => {
851
+ this.scale_value = scale;
852
+ });
853
+ this.position.subscribe((position) => {
854
+ this.position_value = position;
855
+ });
856
+ this.app.ticker.add(() => {
857
+ this.image_container.scale.set(this.scale_value);
858
+ this.image_container.position.set(this.position_value.x, this.position_value.y);
859
+ const effective_width = this.dimensions_value.width * this.scale_value;
860
+ const effective_height = this.dimensions_value.height * this.scale_value;
861
+ // Convert image_container's position (in stage space) to outline_container local coordinates.
862
+ const local_x = Math.round(this.image_container.position.x - this.outline_container.position.x);
863
+ const local_y = Math.round(this.image_container.position.y - this.outline_container.position.y);
864
+ // Make sure overlay container follows the same position as outline container
865
+ this.overlay_container.position.set(this.outline_container.position.x, this.outline_container.position.y);
866
+ this.outline_graphics.clear();
867
+ this.outline_graphics
868
+ .rect(local_x, local_y, effective_width, effective_height)
869
+ .fill({
870
+ color: this.dark ? 0x333333 : 0xffffff,
871
+ alpha: 1
872
+ });
873
+ // Draw border region indicator if border_region > 0
874
+ if (this.border_region > 0) {
875
+ const scaled_border = this.border_region * this.scale_value;
876
+ const border_x = local_x + scaled_border - 1;
877
+ const border_y = local_y + scaled_border - 1;
878
+ const border_width = effective_width - scaled_border * 2 + 1;
879
+ const border_height = effective_height - scaled_border * 2 + 1;
880
+ // Clear previous overlay graphics
881
+ this.overlay_graphics.clear();
882
+ // Draw border rectangle using pixelLine for crisp 1px outline
883
+ this.overlay_graphics
884
+ .rect(border_x, border_y, border_width, border_height)
885
+ .stroke({
886
+ color: 0x999999,
887
+ width: 1,
888
+ alpha: 0,
889
+ pixelLine: true
890
+ });
891
+ // Create dashed line effect by drawing small rects along the border
892
+ const dashLength = 5;
893
+ const gapLength = 5;
894
+ const totalLength = dashLength + gapLength;
895
+ const lineColor = 0x999999;
896
+ // Draw dashed horizontal lines (top and bottom)
897
+ for (let x = border_x; x < border_x + border_width; x += totalLength) {
898
+ // Top dash
899
+ this.overlay_graphics
900
+ .rect(x, border_y, Math.min(dashLength, border_x + border_width - x), 1)
901
+ .fill({ color: lineColor, alpha: 0.7 });
902
+ // Bottom dash
903
+ this.overlay_graphics
904
+ .rect(x, border_y + border_height, Math.min(dashLength, border_x + border_width - x), 1)
905
+ .fill({ color: lineColor, alpha: 0.7 });
906
+ }
907
+ // Draw dashed vertical lines (left and right)
908
+ for (let y = border_y; y < border_y + border_height; y += totalLength) {
909
+ // Left dash
910
+ this.overlay_graphics
911
+ .rect(border_x, y, 1, Math.min(dashLength, border_y + border_height - y))
912
+ .fill({ color: lineColor, alpha: 0.7 });
913
+ // Right dash
914
+ this.overlay_graphics
915
+ .rect(border_x + border_width, y, 1, Math.min(dashLength, border_y + border_height - y))
916
+ .fill({ color: lineColor, alpha: 0.7 });
917
+ }
918
+ }
919
+ else {
920
+ // Clear overlay graphics if no border region
921
+ this.overlay_graphics.clear();
922
+ }
923
+ });
924
+ this.ready_resolve();
925
+ }
926
+ async setup_containers() {
927
+ this.image_container = new Container({
928
+ eventMode: "static",
929
+ sortableChildren: true
930
+ });
931
+ this.image_container.width = this.width;
932
+ this.image_container.height = this.height;
933
+ this.app.stage.sortableChildren = true;
934
+ this.app.stage.alpha = 1;
935
+ this.app.stage.addChild(this.image_container);
936
+ this.image_container.scale.set(1);
937
+ this.ui_container = new Container({
938
+ eventMode: "static"
939
+ });
940
+ this.app.stage.addChild(this.ui_container);
941
+ this.ui_container.width = this.width;
942
+ this.ui_container.height = this.height;
943
+ this.outline_container = new Container();
944
+ this.outline_container.zIndex = -10;
945
+ this.outline_graphics = new Graphics();
946
+ this.outline_graphics.rect(0, 0, this.width, this.height).fill({
947
+ color: this.dark ? 0x333333 : 0xffffff,
948
+ alpha: 0
949
+ });
950
+ if (!this.dark) {
951
+ const blurFilter = new BlurFilter({
952
+ alpha: 0.1,
953
+ blur: 2,
954
+ color: 0x000000,
955
+ offset: { x: 0, y: 0 },
956
+ quality: 4,
957
+ shadowOnly: false
958
+ });
959
+ this.outline_graphics.filters = [blurFilter];
960
+ }
961
+ this.outline_container.addChild(this.outline_graphics);
962
+ this.app.stage.addChild(this.outline_container);
963
+ this.overlay_container = new Container();
964
+ this.app.stage.addChild(this.overlay_container);
965
+ this.overlay_container.width = this.width;
966
+ this.overlay_container.height = this.height;
967
+ this.overlay_container.zIndex = 999;
968
+ this.overlay_container.eventMode = "static";
969
+ this.overlay_container.interactiveChildren = true;
970
+ this.overlay_graphics = new Graphics();
971
+ this.overlay_container.addChild(this.overlay_graphics);
972
+ this.overlay_graphics.rect(0, 0, this.width, this.height).fill({
973
+ color: 0x000000,
974
+ alpha: 0
975
+ });
976
+ const app_center_x = this.app.screen.width / 2;
977
+ const app_center_y = this.app.screen.height / 2;
978
+ this.image_container.position.set(app_center_x, app_center_y);
979
+ this.outline_container.position.set(app_center_x, app_center_y);
980
+ this.layer_manager = new LayerManager(this.image_container, this.app, this.fixed_canvas, this.dark, this.border_region, this.layer_options);
981
+ this.layers = this.layer_manager.layer_store;
982
+ }
983
+ // private resize_image(width: number, height: number): void {
984
+ // // Implement image resizing logic here
985
+ // this.image_container.width = width;
986
+ // this.image_container.height = height;
987
+ // }
988
+ resize_canvas(width, height) {
989
+ if (this.app.renderer) {
990
+ this.app.renderer.resize(width, height);
991
+ }
992
+ // Instead, update the container position to stay centered
993
+ const app_center_x = width / 2;
994
+ const app_center_y = height / 2;
995
+ if (this.image_container) {
996
+ this.image_container.position.set(app_center_x, app_center_y);
997
+ }
998
+ if (this.outline_container) {
999
+ this.outline_container.position.set(app_center_x, app_center_y);
1000
+ }
1001
+ }
1002
+ reset() {
1003
+ const zoom = this.tools.get("zoom");
1004
+ if (zoom) {
1005
+ zoom.cleanup();
1006
+ zoom.setup(this.context, this.current_tool, this.current_subtool);
1007
+ }
1008
+ }
1009
+ set_image_properties(properties) {
1010
+ let hard = typeof properties.animate !== "undefined" ? !properties.animate : true;
1011
+ if (properties.position) {
1012
+ this.position.set(properties.position, { hard });
1013
+ }
1014
+ if (properties.scale) {
1015
+ this.scale.set(properties.scale, { hard });
1016
+ }
1017
+ if (properties.width && properties.height) {
1018
+ this.width = properties.width;
1019
+ this.height = properties.height;
1020
+ this.dimensions.set({ width: properties.width, height: properties.height }, { hard });
1021
+ }
1022
+ }
1023
+ execute_command(command) {
1024
+ this.command_manager.execute(command);
1025
+ }
1026
+ undo() {
1027
+ this.command_manager.undo();
1028
+ this.notify("change");
1029
+ }
1030
+ redo() {
1031
+ this.command_manager.redo();
1032
+ this.notify("change");
1033
+ }
1034
+ async add_image({ image, resize = true }) {
1035
+ const image_command = new AddImageCommand(this.context, image, this.fixed_canvas, this.border_region);
1036
+ await image_command.start();
1037
+ this.context.execute_command(image_command);
1038
+ }
1039
+ /**
1040
+ * Adds an image from a URL as the background layer
1041
+ * @param url The URL of the image to add
1042
+ */
1043
+ async add_image_from_url(url) {
1044
+ const image_tool = this.tools.get("image");
1045
+ const texture = await Assets.load(url);
1046
+ await image_tool.add_image({
1047
+ image: texture,
1048
+ fixed_canvas: this.fixed_canvas,
1049
+ border_region: this.border_region
1050
+ });
1051
+ // Update resize tool if present
1052
+ const resize_tool = this.tools.get("resize");
1053
+ if (resize_tool && typeof resize_tool.set_border_region === "function") {
1054
+ resize_tool.set_border_region(this.border_region);
1055
+ }
1056
+ this.notify("change");
1057
+ this.notify("input");
1058
+ }
1059
+ set_tool(tool) {
1060
+ this.current_tool = tool;
1061
+ for (const tool of this.tools.values()) {
1062
+ tool.set_tool(this.current_tool, this.current_subtool);
1063
+ }
1064
+ }
1065
+ set_subtool(subtool) {
1066
+ this.current_subtool = subtool;
1067
+ for (const tool of this.tools.values()) {
1068
+ tool.set_tool(this.current_tool, this.current_subtool);
1069
+ }
1070
+ }
1071
+ set_background_image(image) {
1072
+ this.background_image = image;
1073
+ }
1074
+ reset_canvas() {
1075
+ this.layer_manager.reset_layers(this.width, this.height);
1076
+ // Clear background image
1077
+ this.background_image = undefined;
1078
+ this.background_image_present.set(false);
1079
+ // Reset position, scale and dimensions to default values
1080
+ this.set_image_properties({
1081
+ width: this.width,
1082
+ height: this.height,
1083
+ scale: 1,
1084
+ position: { x: 0, y: 0 },
1085
+ animate: false
1086
+ });
1087
+ // Reset command stacks
1088
+ this.command_manager = new CommandManager();
1089
+ //create background layer
1090
+ this.layer_manager.create_background_layer(this.width, this.height);
1091
+ // Reset tools
1092
+ for (const tool of this.tools.values()) {
1093
+ tool.cleanup();
1094
+ tool.setup(this.context, this.current_tool, this.current_subtool);
1095
+ }
1096
+ const zoom_tool = this.tools.get("zoom");
1097
+ if (zoom_tool) {
1098
+ zoom_tool.min_zoom.subscribe((is_min_zoom) => {
1099
+ this.min_zoom.set(is_min_zoom);
1100
+ });
1101
+ }
1102
+ this.notify("change");
1103
+ }
1104
+ add_layer() {
1105
+ this.layer_manager.create_layer({
1106
+ width: this.width,
1107
+ height: this.height,
1108
+ layer_name: undefined,
1109
+ user_created: true
1110
+ });
1111
+ this.notify("change");
1112
+ }
1113
+ /**
1114
+ * Adds a new layer with an image loaded from a URL
1115
+ * @param layer_urls The URLs of the images to load
1116
+ * @returns A Promise that resolves when all layers are added
1117
+ */
1118
+ async add_layers_from_url(layer_urls) {
1119
+ const _layers = this.layer_manager.get_layers();
1120
+ _layers.forEach((l) => this.layer_manager.delete_layer(l.id));
1121
+ if (layer_urls === undefined || layer_urls.length === 0) {
1122
+ this.layer_manager.create_layer({
1123
+ width: this.width,
1124
+ height: this.height,
1125
+ layer_name: undefined,
1126
+ user_created: false
1127
+ });
1128
+ return;
1129
+ }
1130
+ for await (const url of layer_urls) {
1131
+ await this.layer_manager.add_layer_from_url(url);
1132
+ }
1133
+ // Update resize tool if present with border region
1134
+ const resize_tool = this.tools.get("resize");
1135
+ if (resize_tool && typeof resize_tool.set_border_region === "function") {
1136
+ resize_tool.set_border_region(this.border_region);
1137
+ }
1138
+ this.notify("change");
1139
+ this.notify("input");
1140
+ }
1141
+ set_layer(id) {
1142
+ this.layer_manager.set_active_layer(id);
1143
+ this.notify("change");
1144
+ }
1145
+ move_layer(id, direction) {
1146
+ this.layer_manager.move_layer(id, direction);
1147
+ this.notify("change");
1148
+ }
1149
+ delete_layer(id) {
1150
+ this.layer_manager.delete_layer(id);
1151
+ this.notify("change");
1152
+ }
1153
+ modify_canvas_size(width, height, anchor, scale) {
1154
+ // Store old dimensions before updating
1155
+ const oldWidth = this.width;
1156
+ const oldHeight = this.height;
1157
+ // Resize layers first, passing old dimensions for anchor calculation
1158
+ this.layer_manager.resize_all_layers(width, height, scale, anchor, oldWidth, oldHeight);
1159
+ // Update the editor's main dimension tracking *after* resizing layers
1160
+ this.width = width;
1161
+ this.height = height;
1162
+ // Update the dimensions store and reset view scale/position
1163
+ this.set_image_properties({
1164
+ width,
1165
+ height,
1166
+ scale: 1, // Reset scale
1167
+ position: { x: 0, y: 0 }, // Reset pan/position of the image_container
1168
+ animate: false
1169
+ });
1170
+ this.notify("change");
1171
+ }
1172
+ async get_blobs() {
1173
+ const blobs = await this.layer_manager.get_blobs(this.width, this.height);
1174
+ return blobs;
1175
+ }
1176
+ on(event, callback) {
1177
+ this.event_callbacks.set(event, [
1178
+ ...(this.event_callbacks.get(event) || []),
1179
+ callback
1180
+ ]);
1181
+ }
1182
+ off(event, callback) {
1183
+ this.event_callbacks.set(event, this.event_callbacks.get(event)?.filter((cb) => cb !== callback) || []);
1184
+ }
1185
+ notify(event) {
1186
+ for (const callback of this.event_callbacks.get(event) || []) {
1187
+ callback();
1188
+ }
1189
+ }
1190
+ destroy() {
1191
+ if (!this.app)
1192
+ return;
1193
+ // for (const tool of this.tools) {
1194
+ // tool[1]?.cleanup();
1195
+ // }
1196
+ this.app?.destroy();
1197
+ }
1198
+ resize(width, height) {
1199
+ this.set_image_properties({
1200
+ width,
1201
+ height
1202
+ });
1203
+ this.reset();
1204
+ }
1205
+ async get_crop_bounds() {
1206
+ const crop_tool = this.tools.get("crop");
1207
+ const crop_bounds = crop_tool.get_crop_bounds();
1208
+ const image = await crop_tool.get_image();
1209
+ return {
1210
+ image,
1211
+ ...crop_bounds
1212
+ };
1213
+ }
1214
+ get background_image_sprite() {
1215
+ return this.background_image;
1216
+ }
1217
+ set_layer_options(layer_options) {
1218
+ this.layer_options = layer_options;
1219
+ this.layer_manager.set_layer_options(layer_options, this.width, this.height);
1220
+ }
1221
+ }