@gradio/imageeditor 0.12.13 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/Example.svelte +1 -1
- package/IMAGE_EDITOR_OVERVIEW.md +224 -0
- package/ImageEditor.stories.svelte +4 -4
- package/Index.svelte +35 -46
- package/{shared/InteractiveImageEditor.svelte → InteractiveImageEditor.svelte} +56 -103
- package/dist/Example.svelte.d.ts +1 -1
- package/dist/Index.svelte +29 -33
- package/dist/Index.svelte.d.ts +17 -16
- package/dist/{shared/InteractiveImageEditor.svelte → InteractiveImageEditor.svelte} +47 -88
- package/dist/{shared/InteractiveImageEditor.svelte.d.ts → InteractiveImageEditor.svelte.d.ts} +13 -15
- package/dist/shared/Anchor.svelte +311 -0
- package/dist/shared/Anchor.svelte.d.ts +18 -0
- package/dist/shared/Controls.svelte +162 -23
- package/dist/shared/Controls.svelte.d.ts +7 -2
- package/dist/shared/{tools/IconButton.svelte → IconButton.svelte} +19 -3
- package/dist/shared/{tools/IconButton.svelte.d.ts → IconButton.svelte.d.ts} +4 -1
- package/dist/shared/ImageEditor.svelte +546 -289
- package/dist/shared/ImageEditor.svelte.d.ts +51 -56
- package/dist/shared/Layers.svelte +233 -0
- package/dist/shared/Layers.svelte.d.ts +35 -0
- package/dist/shared/Resize.svelte +218 -0
- package/dist/shared/Resize.svelte.d.ts +29 -0
- package/dist/shared/SecondaryToolbar.svelte +54 -0
- package/dist/shared/SecondaryToolbar.svelte.d.ts +35 -0
- package/dist/shared/Toolbar.svelte +337 -0
- package/dist/shared/Toolbar.svelte.d.ts +42 -0
- package/dist/shared/{tools → brush}/BrushOptions.svelte +68 -64
- package/dist/shared/{tools → brush}/BrushOptions.svelte.d.ts +9 -9
- package/dist/shared/{tools → brush}/BrushSize.svelte +2 -27
- package/dist/shared/{tools → brush}/ColorField.svelte +2 -1
- package/dist/shared/{tools → brush}/ColorField.svelte.d.ts +1 -0
- package/dist/shared/{tools → brush}/ColorPicker.svelte +81 -9
- package/dist/shared/{tools → brush}/ColorPicker.svelte.d.ts +1 -0
- package/dist/shared/{tools → brush}/ColorSwatch.svelte +78 -38
- package/dist/shared/{tools → brush}/ColorSwatch.svelte.d.ts +6 -2
- package/dist/shared/brush/brush-cursor.d.ts +101 -0
- package/dist/shared/brush/brush-cursor.js +327 -0
- package/dist/shared/brush/brush-textures.d.ts +71 -0
- package/dist/shared/brush/brush-textures.js +404 -0
- package/dist/shared/brush/brush-utils.d.ts +32 -0
- package/dist/shared/brush/brush-utils.js +59 -0
- package/dist/shared/brush/brush.d.ts +152 -0
- package/dist/shared/brush/brush.js +396 -0
- package/dist/shared/brush/types.d.ts +37 -0
- package/dist/shared/brush/types.js +1 -0
- package/dist/shared/core/editor.d.ts +252 -0
- package/dist/shared/core/editor.js +1221 -0
- package/dist/shared/crop/crop.d.ts +201 -0
- package/dist/shared/crop/crop.js +727 -0
- package/dist/shared/image/image.d.ts +89 -0
- package/dist/shared/image/image.js +185 -0
- package/dist/shared/resize/resize.d.ts +270 -0
- package/dist/shared/resize/resize.js +1413 -0
- package/dist/shared/types.d.ts +12 -0
- package/dist/shared/utils/pixi.d.ts +16 -57
- package/dist/shared/utils/pixi.js +15 -113
- package/dist/shared/zoom/zoom.d.ts +136 -0
- package/dist/shared/zoom/zoom.js +548 -0
- package/package.json +11 -10
- package/shared/Anchor.svelte +384 -0
- package/shared/Controls.svelte +194 -27
- package/shared/{tools/IconButton.svelte → IconButton.svelte} +21 -4
- package/shared/ImageEditor.svelte +682 -349
- package/shared/Layers.svelte +249 -0
- package/shared/Resize.svelte +239 -0
- package/shared/SecondaryToolbar.svelte +61 -0
- package/shared/Toolbar.svelte +386 -0
- package/shared/brush/BRUSH_TOOL.md +134 -0
- package/shared/brush/BrushOptions.svelte +200 -0
- package/shared/{tools → brush}/BrushSize.svelte +2 -30
- package/shared/{tools → brush}/ColorField.svelte +3 -1
- package/shared/{tools → brush}/ColorPicker.svelte +86 -9
- package/shared/{tools → brush}/ColorSwatch.svelte +95 -42
- package/shared/brush/brush-cursor.ts +408 -0
- package/shared/brush/brush-textures.ts +508 -0
- package/shared/brush/brush-utils.ts +76 -0
- package/shared/brush/brush.ts +506 -0
- package/shared/brush/types.ts +41 -0
- package/shared/core/EDITOR.md +182 -0
- package/shared/core/editor.ts +1730 -0
- package/shared/crop/CROP.md +139 -0
- package/shared/crop/crop.ts +1027 -0
- package/shared/image/IMAGE.md +135 -0
- package/shared/image/image.ts +300 -0
- package/shared/resize/resize.ts +1894 -0
- package/shared/types.ts +14 -0
- package/shared/utils/pixi.ts +19 -246
- package/shared/zoom/zoom.ts +729 -0
- package/dist/shared/layers/Layers.svelte +0 -231
- package/dist/shared/layers/Layers.svelte.d.ts +0 -18
- package/dist/shared/layers/utils.d.ts +0 -73
- package/dist/shared/layers/utils.js +0 -156
- package/dist/shared/tools/Brush.svelte +0 -215
- package/dist/shared/tools/Brush.svelte.d.ts +0 -29
- package/dist/shared/tools/Crop.svelte +0 -155
- package/dist/shared/tools/Crop.svelte.d.ts +0 -16
- package/dist/shared/tools/Cropper.svelte +0 -271
- package/dist/shared/tools/Cropper.svelte.d.ts +0 -41
- package/dist/shared/tools/Handle.svelte +0 -260
- package/dist/shared/tools/Handle.svelte.d.ts +0 -29
- package/dist/shared/tools/Sources.svelte +0 -257
- package/dist/shared/tools/Sources.svelte.d.ts +0 -34
- package/dist/shared/tools/Tools.svelte +0 -102
- package/dist/shared/tools/Tools.svelte.d.ts +0 -48
- package/dist/shared/tools/brush.d.ts +0 -62
- package/dist/shared/tools/brush.js +0 -133
- package/dist/shared/tools/crop.d.ts +0 -15
- package/dist/shared/tools/crop.js +0 -154
- package/dist/shared/tools/index.d.ts +0 -5
- package/dist/shared/tools/index.js +0 -5
- package/dist/shared/tools/sources.d.ts +0 -46
- package/dist/shared/tools/sources.js +0 -121
- package/dist/shared/tools/types.d.ts +0 -1
- package/shared/layers/Layers.svelte +0 -250
- package/shared/layers/utils.ts +0 -294
- package/shared/tools/Brush.svelte +0 -280
- package/shared/tools/BrushOptions.svelte +0 -180
- package/shared/tools/Crop.svelte +0 -189
- package/shared/tools/Cropper.svelte +0 -346
- package/shared/tools/Handle.svelte +0 -289
- package/shared/tools/Sources.svelte +0 -283
- package/shared/tools/Tools.svelte +0 -162
- package/shared/tools/brush.ts +0 -247
- package/shared/tools/crop.ts +0 -213
- package/shared/tools/index.ts +0 -5
- package/shared/tools/sources.ts +0 -195
- package/shared/tools/types.ts +0 -1
- /package/dist/shared/{tools → brush}/BrushSize.svelte.d.ts +0 -0
- /package/dist/shared/{tools/types.js → types.js} +0 -0
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
import { Container, Graphics, Point, Rectangle, FederatedPointerEvent, Sprite } from "pixi.js";
|
|
2
|
+
import {} from "../core/editor";
|
|
3
|
+
import {} from "../Toolbar.svelte";
|
|
4
|
+
import { get_canvas_blob } from "../utils/pixi";
|
|
5
|
+
/**
|
|
6
|
+
* @class CropTool
|
|
7
|
+
* @implements {Tool}
|
|
8
|
+
* @description Implements the crop tool functionality for the image editor
|
|
9
|
+
*/
|
|
10
|
+
export class CropTool {
|
|
11
|
+
name = "crop";
|
|
12
|
+
image_editor_context;
|
|
13
|
+
current_tool = "image";
|
|
14
|
+
current_subtool = "crop";
|
|
15
|
+
// Constants
|
|
16
|
+
CORNER_SIZE = 25;
|
|
17
|
+
LINE_THICKNESS = 5;
|
|
18
|
+
HANDLE_COLOR = 0x000000;
|
|
19
|
+
HIT_AREA_SIZE = 40;
|
|
20
|
+
// State
|
|
21
|
+
is_dragging = false;
|
|
22
|
+
selected_handle = null;
|
|
23
|
+
active_corner_index = -1;
|
|
24
|
+
active_edge_index = -1;
|
|
25
|
+
last_pointer_position = null;
|
|
26
|
+
crop_bounds = { x: 0, y: 0, width: 0, height: 0 };
|
|
27
|
+
crop_ui_container = null;
|
|
28
|
+
crop_mask = null;
|
|
29
|
+
dimensions = {
|
|
30
|
+
width: 0,
|
|
31
|
+
height: 0
|
|
32
|
+
};
|
|
33
|
+
position = { x: 0, y: 0 };
|
|
34
|
+
scale = 1;
|
|
35
|
+
is_dragging_window = false;
|
|
36
|
+
drag_start_position = null;
|
|
37
|
+
drag_start_bounds = null;
|
|
38
|
+
background_image_watcher = null;
|
|
39
|
+
has_been_manually_changed = false;
|
|
40
|
+
event_callbacks = new Map();
|
|
41
|
+
/**
|
|
42
|
+
* @method setup
|
|
43
|
+
* @async
|
|
44
|
+
* @description Sets up the crop tool with the given context and state
|
|
45
|
+
* @param {ImageEditorContext} context - The image editor context
|
|
46
|
+
* @param {ToolbarTool} tool - The current tool
|
|
47
|
+
* @param {Subtool} subtool - The current subtool
|
|
48
|
+
* @returns {Promise<void>}
|
|
49
|
+
*/
|
|
50
|
+
async setup(context, tool, subtool) {
|
|
51
|
+
this.image_editor_context = context;
|
|
52
|
+
this.current_tool = tool;
|
|
53
|
+
this.current_subtool = subtool;
|
|
54
|
+
context.dimensions.subscribe((dimensions) => {
|
|
55
|
+
this.dimensions = dimensions;
|
|
56
|
+
// Update crop bounds to match new dimensions
|
|
57
|
+
this.crop_bounds = {
|
|
58
|
+
x: 0,
|
|
59
|
+
y: 0,
|
|
60
|
+
width: dimensions.width,
|
|
61
|
+
height: dimensions.height
|
|
62
|
+
};
|
|
63
|
+
// Force UI update if it exists
|
|
64
|
+
if (this.crop_ui_container) {
|
|
65
|
+
this.update_crop_ui();
|
|
66
|
+
}
|
|
67
|
+
// Always update the mask when dimensions change
|
|
68
|
+
this.set_crop_mask();
|
|
69
|
+
this.update_crop_mask();
|
|
70
|
+
});
|
|
71
|
+
context.position.subscribe((position) => {
|
|
72
|
+
this.position = position;
|
|
73
|
+
});
|
|
74
|
+
context.scale.subscribe((scale) => {
|
|
75
|
+
this.scale = scale;
|
|
76
|
+
});
|
|
77
|
+
// Setup a background image watcher to ensure we always apply the mask to the current background image
|
|
78
|
+
this.background_image_watcher = () => {
|
|
79
|
+
// Only apply when crop tool is active and we have a mask
|
|
80
|
+
if (this.crop_mask &&
|
|
81
|
+
this.current_tool === "image" &&
|
|
82
|
+
this.current_subtool === "crop") {
|
|
83
|
+
// Get a fresh reference to the background image and apply the mask
|
|
84
|
+
if (this.image_editor_context.background_image) {
|
|
85
|
+
this.image_editor_context.background_image.setMask(this.crop_mask);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
// Add the watcher to the ticker to regularly check for background image changes
|
|
90
|
+
this.image_editor_context.app.ticker.add(this.background_image_watcher);
|
|
91
|
+
// Initialize crop UI
|
|
92
|
+
await this.init_crop_ui();
|
|
93
|
+
// Setup event listeners
|
|
94
|
+
this.setup_event_listeners();
|
|
95
|
+
// Ensure the crop UI visibility matches the current tool state
|
|
96
|
+
const isCropActive = tool === "image" && subtool === "crop";
|
|
97
|
+
if (this.crop_ui_container) {
|
|
98
|
+
this.crop_ui_container.visible = isCropActive;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* @method cleanup
|
|
103
|
+
* @description Cleans up the crop tool resources and removes event listeners
|
|
104
|
+
* @returns {void}
|
|
105
|
+
*/
|
|
106
|
+
cleanup() {
|
|
107
|
+
// Remove the crop UI container
|
|
108
|
+
if (this.crop_ui_container) {
|
|
109
|
+
this.image_editor_context.app.stage.removeChild(this.crop_ui_container);
|
|
110
|
+
}
|
|
111
|
+
// Remove the mask and reset the image container's mask property
|
|
112
|
+
if (this.crop_mask) {
|
|
113
|
+
if (this.crop_mask.parent) {
|
|
114
|
+
this.crop_mask.parent.removeChild(this.crop_mask);
|
|
115
|
+
}
|
|
116
|
+
// Clear mask from background_image if applied
|
|
117
|
+
if (this.image_editor_context.background_image &&
|
|
118
|
+
this.image_editor_context.background_image.mask === this.crop_mask) {
|
|
119
|
+
this.image_editor_context.background_image.mask = null;
|
|
120
|
+
}
|
|
121
|
+
// Clear mask from image_container if applied
|
|
122
|
+
if (this.image_editor_context.image_container.mask === this.crop_mask) {
|
|
123
|
+
this.image_editor_context.image_container.mask = null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Ensure all references to the crop mask are cleared
|
|
127
|
+
this.crop_mask = null;
|
|
128
|
+
this.crop_ui_container = null;
|
|
129
|
+
// Remove the background image watcher from the ticker
|
|
130
|
+
if (this.background_image_watcher) {
|
|
131
|
+
this.image_editor_context.app.ticker.remove(this.background_image_watcher);
|
|
132
|
+
this.background_image_watcher = null;
|
|
133
|
+
}
|
|
134
|
+
// Remove event listeners
|
|
135
|
+
this.cleanup_event_listeners();
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* @method set_tool
|
|
139
|
+
* @description Sets the current tool and subtool, updating UI visibility
|
|
140
|
+
* @param {ToolbarTool} tool - The tool to set
|
|
141
|
+
* @param {Subtool} subtool - The subtool to set
|
|
142
|
+
* @returns {void}
|
|
143
|
+
*/
|
|
144
|
+
set_tool(tool, subtool) {
|
|
145
|
+
this.current_tool = tool;
|
|
146
|
+
this.current_subtool = subtool;
|
|
147
|
+
// Show/hide crop UI based on tool and subtool
|
|
148
|
+
const isCropActive = tool === "image" && subtool === "crop";
|
|
149
|
+
// Only update the UI visibility, not the mask
|
|
150
|
+
if (this.crop_ui_container) {
|
|
151
|
+
this.crop_ui_container.visible = isCropActive;
|
|
152
|
+
}
|
|
153
|
+
// If crop tool is active, ensure the mask is up to date
|
|
154
|
+
if (isCropActive && this.crop_mask) {
|
|
155
|
+
this.update_crop_mask();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* @method set_crop_mask
|
|
160
|
+
* @private
|
|
161
|
+
* @description Sets up the crop mask for the image
|
|
162
|
+
* @returns {void}
|
|
163
|
+
*/
|
|
164
|
+
set_crop_mask() {
|
|
165
|
+
// Remove any existing mask to avoid duplicates
|
|
166
|
+
if (this.crop_mask) {
|
|
167
|
+
if (this.crop_mask.parent) {
|
|
168
|
+
this.crop_mask.parent.removeChild(this.crop_mask);
|
|
169
|
+
}
|
|
170
|
+
// Clear mask from image_container if it was applied there
|
|
171
|
+
if (this.image_editor_context.image_container.mask === this.crop_mask) {
|
|
172
|
+
this.image_editor_context.image_container.mask = null;
|
|
173
|
+
}
|
|
174
|
+
// Clear mask from background_image if it was applied there
|
|
175
|
+
if (this.image_editor_context.background_image &&
|
|
176
|
+
this.image_editor_context.background_image.mask === this.crop_mask) {
|
|
177
|
+
this.image_editor_context.background_image.mask = null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Create a new mask that will actually be used as an overlay
|
|
181
|
+
this.crop_mask = new Graphics();
|
|
182
|
+
// Add the mask/overlay to the display list
|
|
183
|
+
// This time it should be visible since we want to see the semi-transparent overlay
|
|
184
|
+
this.image_editor_context.image_container.addChild(this.crop_mask);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* @method init_crop_ui
|
|
188
|
+
* @private
|
|
189
|
+
* @async
|
|
190
|
+
* @description Initializes the crop UI elements and sets up the update loop
|
|
191
|
+
* @returns {Promise<void>}
|
|
192
|
+
*/
|
|
193
|
+
async init_crop_ui() {
|
|
194
|
+
// Create crop UI with scaled dimensions
|
|
195
|
+
const { width, height } = this.image_editor_context.background_image?.getLocalBounds() || {
|
|
196
|
+
width: false,
|
|
197
|
+
height: false
|
|
198
|
+
};
|
|
199
|
+
this.crop_ui_container = this.make_crop_ui(this.dimensions.width * this.scale, this.dimensions.height * this.scale);
|
|
200
|
+
// Position the crop UI container at the image position
|
|
201
|
+
this.crop_ui_container.position.set(this.position.x, this.position.y);
|
|
202
|
+
// Initially hide the crop UI until explicitly enabled by set_tool
|
|
203
|
+
this.crop_ui_container.visible = false;
|
|
204
|
+
this.image_editor_context.app.stage.addChild(this.crop_ui_container);
|
|
205
|
+
// Create and apply mask to the image container
|
|
206
|
+
this.set_crop_mask();
|
|
207
|
+
// Start the update loop for the UI
|
|
208
|
+
this.image_editor_context.app.ticker.add(this.update_crop_ui.bind(this));
|
|
209
|
+
// Initialize the mask with the current crop bounds
|
|
210
|
+
this.update_crop_mask();
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* @method make_crop_ui
|
|
214
|
+
* @private
|
|
215
|
+
* @description Creates the crop UI with the specified dimensions
|
|
216
|
+
* @param {number} width - Width of the crop area
|
|
217
|
+
* @param {number} height - Height of the crop area
|
|
218
|
+
* @returns {Container} The created crop UI container
|
|
219
|
+
*/
|
|
220
|
+
make_crop_ui(width, height) {
|
|
221
|
+
const crop_container = new Container();
|
|
222
|
+
crop_container.eventMode = "static";
|
|
223
|
+
crop_container.interactiveChildren = true;
|
|
224
|
+
// Create crop outline with hit area for dragging
|
|
225
|
+
const crop_outline = new Graphics()
|
|
226
|
+
.rect(0, 0, width, height)
|
|
227
|
+
.stroke({ width: 1, color: 0x000000, alignment: 0, alpha: 0.5 });
|
|
228
|
+
// Make the outline interactive
|
|
229
|
+
crop_outline.eventMode = "static";
|
|
230
|
+
crop_outline.cursor = "move";
|
|
231
|
+
// Set hit area to match the outline's dimensions
|
|
232
|
+
crop_outline.hitArea = new Rectangle(0, 0, width, height);
|
|
233
|
+
// Add drag handlers to the outline
|
|
234
|
+
crop_outline.on("pointerdown", this.handle_window_drag_start.bind(this));
|
|
235
|
+
crop_container.addChild(crop_outline);
|
|
236
|
+
// Create corner handles
|
|
237
|
+
this.create_corner_handles(crop_container, width, height);
|
|
238
|
+
// Create edge handles
|
|
239
|
+
this.create_edge_handles(crop_container, width, height);
|
|
240
|
+
return crop_container;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* @method create_handle
|
|
244
|
+
* @private
|
|
245
|
+
* @description Creates a handle for the crop UI
|
|
246
|
+
* @param {boolean} [is_edge=false] - Whether the handle is an edge handle
|
|
247
|
+
* @returns {Container} The created handle container
|
|
248
|
+
*/
|
|
249
|
+
create_handle(is_edge = false) {
|
|
250
|
+
const handle = new Container();
|
|
251
|
+
handle.eventMode = "static";
|
|
252
|
+
const handle_graphics = new Graphics();
|
|
253
|
+
if (is_edge) {
|
|
254
|
+
// Create a shorter bar for edge handles (2x instead of 3x)
|
|
255
|
+
handle_graphics
|
|
256
|
+
.rect(0, 0, this.CORNER_SIZE * 1.5, this.LINE_THICKNESS)
|
|
257
|
+
.fill(this.HANDLE_COLOR);
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
// Create L-shaped corner handles
|
|
261
|
+
handle_graphics
|
|
262
|
+
.rect(0, 0, this.CORNER_SIZE, this.LINE_THICKNESS)
|
|
263
|
+
.rect(0, 0, this.LINE_THICKNESS, this.CORNER_SIZE)
|
|
264
|
+
.fill(this.HANDLE_COLOR);
|
|
265
|
+
}
|
|
266
|
+
handle.addChild(handle_graphics);
|
|
267
|
+
// Adjust hit area based on handle type
|
|
268
|
+
const hit_size = is_edge ? this.HIT_AREA_SIZE * 1.5 : this.HIT_AREA_SIZE;
|
|
269
|
+
handle.hitArea = new Rectangle(-hit_size / 2 + this.LINE_THICKNESS, -hit_size / 2 + this.LINE_THICKNESS, hit_size, hit_size);
|
|
270
|
+
return handle;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* @method create_edge_handles
|
|
274
|
+
* @private
|
|
275
|
+
* @description Creates edge handles for the crop UI
|
|
276
|
+
* @param {Container} container - The container to add handles to
|
|
277
|
+
* @param {number} width - Width of the crop area
|
|
278
|
+
* @param {number} height - Height of the crop area
|
|
279
|
+
* @returns {void}
|
|
280
|
+
*/
|
|
281
|
+
create_edge_handles(container, width, height) {
|
|
282
|
+
// Horizontal handles at top and bottom
|
|
283
|
+
[-1, 1].forEach((i, index) => {
|
|
284
|
+
const handle = this.create_handle(true); // true for edge handle
|
|
285
|
+
handle.rotation = 0;
|
|
286
|
+
handle.on("pointerdown", (event) => {
|
|
287
|
+
this.handle_pointer_down(event, handle, -1, index); // 0 for top, 1 for bottom
|
|
288
|
+
});
|
|
289
|
+
// Center the handle
|
|
290
|
+
handle.x = width / 2 - (this.CORNER_SIZE * 3) / 2;
|
|
291
|
+
handle.y = i < 0 ? -this.LINE_THICKNESS : height;
|
|
292
|
+
handle.cursor = "ns-resize";
|
|
293
|
+
container.addChild(handle);
|
|
294
|
+
});
|
|
295
|
+
// Vertical handles at left and right
|
|
296
|
+
[-1, 1].forEach((i, index) => {
|
|
297
|
+
const handle = this.create_handle(true); // true for edge handle
|
|
298
|
+
handle.rotation = Math.PI / 2; // Rotate 90 degrees
|
|
299
|
+
handle.on("pointerdown", (event) => {
|
|
300
|
+
this.handle_pointer_down(event, handle, -1, index + 2); // 2 for left, 3 for right
|
|
301
|
+
});
|
|
302
|
+
// Center the handle
|
|
303
|
+
handle.x = i < 0 ? -this.LINE_THICKNESS : width;
|
|
304
|
+
handle.y = height / 2 - (this.CORNER_SIZE * 3) / 2;
|
|
305
|
+
handle.cursor = "ew-resize";
|
|
306
|
+
container.addChild(handle);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* @method create_corner_handles
|
|
311
|
+
* @private
|
|
312
|
+
* @description Creates corner handles for the crop UI
|
|
313
|
+
* @param {Container} container - The container to add handles to
|
|
314
|
+
* @param {number} width - Width of the crop area
|
|
315
|
+
* @param {number} height - Height of the crop area
|
|
316
|
+
* @returns {void}
|
|
317
|
+
*/
|
|
318
|
+
create_corner_handles(container, width, height) {
|
|
319
|
+
const corners = [
|
|
320
|
+
{ x: 0, y: 0, xScale: 1, yScale: 1, cursor: "nwse-resize" }, // Top-left
|
|
321
|
+
{ x: width, y: 0, xScale: -1, yScale: 1, cursor: "nesw-resize" }, // Top-right
|
|
322
|
+
{ x: 0, y: height, xScale: 1, yScale: -1, cursor: "nesw-resize" }, // Bottom-left
|
|
323
|
+
{ x: width, y: height, xScale: -1, yScale: -1, cursor: "nwse-resize" } // Bottom-right
|
|
324
|
+
];
|
|
325
|
+
corners.forEach(({ x, y, xScale, yScale, cursor }, i) => {
|
|
326
|
+
const corner = this.create_handle(false); // false for corner handle
|
|
327
|
+
corner.x = x - (xScale < 0 ? -this.LINE_THICKNESS : this.LINE_THICKNESS);
|
|
328
|
+
corner.y = y - (yScale < 0 ? -this.LINE_THICKNESS : this.LINE_THICKNESS);
|
|
329
|
+
corner.scale.set(xScale, yScale);
|
|
330
|
+
corner.on("pointerdown", (event) => {
|
|
331
|
+
this.handle_pointer_down(event, corner, i, -1);
|
|
332
|
+
});
|
|
333
|
+
corner.cursor = cursor;
|
|
334
|
+
container.addChild(corner);
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* @method handle_pointer_down
|
|
339
|
+
* @private
|
|
340
|
+
* @description Handles pointer down events on handles
|
|
341
|
+
* @param {FederatedPointerEvent} event - The pointer event
|
|
342
|
+
* @param {Container} handle - The handle that was clicked
|
|
343
|
+
* @param {number} corner_index - Index of the corner (-1 if not a corner)
|
|
344
|
+
* @param {number} edge_index - Index of the edge (-1 if not an edge)
|
|
345
|
+
* @returns {void}
|
|
346
|
+
*/
|
|
347
|
+
handle_pointer_down(event, handle, corner_index, edge_index) {
|
|
348
|
+
// Only handle crop events if crop tool is active
|
|
349
|
+
if (this.current_subtool !== "crop")
|
|
350
|
+
return;
|
|
351
|
+
event.stopPropagation();
|
|
352
|
+
this.is_dragging = true;
|
|
353
|
+
this.selected_handle = handle;
|
|
354
|
+
this.active_corner_index = corner_index;
|
|
355
|
+
this.active_edge_index = edge_index;
|
|
356
|
+
// Convert global position to the container's local coordinates.
|
|
357
|
+
const local_pos = this.image_editor_context.image_container.toLocal(event.global);
|
|
358
|
+
this.last_pointer_position = new Point(local_pos.x, local_pos.y);
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* updates the crop bounds based on pointer movement
|
|
362
|
+
* @param delta - the change in position
|
|
363
|
+
*/
|
|
364
|
+
update_crop_bounds(delta) {
|
|
365
|
+
// Set flag indicating the crop has been manually changed
|
|
366
|
+
this.has_been_manually_changed = true;
|
|
367
|
+
// Scale the delta to match the image coordinates
|
|
368
|
+
const scaled_delta = new Point(delta.x, delta.y);
|
|
369
|
+
// Store original values to calculate actual changes
|
|
370
|
+
const original = {
|
|
371
|
+
x: this.crop_bounds.x,
|
|
372
|
+
y: this.crop_bounds.y,
|
|
373
|
+
width: this.crop_bounds.width,
|
|
374
|
+
height: this.crop_bounds.height
|
|
375
|
+
};
|
|
376
|
+
if (this.active_corner_index !== -1) {
|
|
377
|
+
// Handle corner dragging
|
|
378
|
+
switch (this.active_corner_index) {
|
|
379
|
+
case 0: // Top-left
|
|
380
|
+
// Update width and x position
|
|
381
|
+
const newWidth0 = Math.max(20, original.width - scaled_delta.x);
|
|
382
|
+
const widthDiff0 = original.width - newWidth0;
|
|
383
|
+
this.crop_bounds.width = newWidth0;
|
|
384
|
+
this.crop_bounds.x = original.x + widthDiff0;
|
|
385
|
+
// Update height and y position
|
|
386
|
+
const newHeight0 = Math.max(20, original.height - scaled_delta.y);
|
|
387
|
+
const heightDiff0 = original.height - newHeight0;
|
|
388
|
+
this.crop_bounds.height = newHeight0;
|
|
389
|
+
this.crop_bounds.y = original.y + heightDiff0;
|
|
390
|
+
break;
|
|
391
|
+
case 1: // Top-right
|
|
392
|
+
// Update width (right side)
|
|
393
|
+
this.crop_bounds.width = Math.max(20, original.width + scaled_delta.x);
|
|
394
|
+
// Update height and y position (top side)
|
|
395
|
+
const newHeight1 = Math.max(20, original.height - scaled_delta.y);
|
|
396
|
+
const heightDiff1 = original.height - newHeight1;
|
|
397
|
+
this.crop_bounds.height = newHeight1;
|
|
398
|
+
this.crop_bounds.y = original.y + heightDiff1;
|
|
399
|
+
break;
|
|
400
|
+
case 2: // Bottom-left
|
|
401
|
+
// Update width and x position (left side)
|
|
402
|
+
const newWidth2 = Math.max(20, original.width - scaled_delta.x);
|
|
403
|
+
const widthDiff2 = original.width - newWidth2;
|
|
404
|
+
this.crop_bounds.width = newWidth2;
|
|
405
|
+
this.crop_bounds.x = original.x + widthDiff2;
|
|
406
|
+
// Update height (bottom side)
|
|
407
|
+
this.crop_bounds.height = Math.max(20, original.height + scaled_delta.y);
|
|
408
|
+
break;
|
|
409
|
+
case 3: // Bottom-right
|
|
410
|
+
// Update width (right side)
|
|
411
|
+
this.crop_bounds.width = Math.max(20, original.width + scaled_delta.x);
|
|
412
|
+
// Update height (bottom side)
|
|
413
|
+
this.crop_bounds.height = Math.max(20, original.height + scaled_delta.y);
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
else if (this.active_edge_index !== -1) {
|
|
418
|
+
// Handle edge dragging
|
|
419
|
+
switch (this.active_edge_index) {
|
|
420
|
+
case 0: // Top
|
|
421
|
+
// Update height and y position
|
|
422
|
+
const newHeight = Math.max(20, original.height - scaled_delta.y);
|
|
423
|
+
const heightDiff = original.height - newHeight;
|
|
424
|
+
this.crop_bounds.height = newHeight;
|
|
425
|
+
this.crop_bounds.y = original.y + heightDiff;
|
|
426
|
+
break;
|
|
427
|
+
case 1: // Bottom
|
|
428
|
+
// Update height only
|
|
429
|
+
this.crop_bounds.height = Math.max(20, original.height + scaled_delta.y);
|
|
430
|
+
break;
|
|
431
|
+
case 2: // Left
|
|
432
|
+
// Update width and x position
|
|
433
|
+
const newWidth = Math.max(20, original.width - scaled_delta.x);
|
|
434
|
+
const widthDiff = original.width - newWidth;
|
|
435
|
+
this.crop_bounds.width = newWidth;
|
|
436
|
+
this.crop_bounds.x = original.x + widthDiff;
|
|
437
|
+
break;
|
|
438
|
+
case 3: // Right
|
|
439
|
+
// Update width only
|
|
440
|
+
this.crop_bounds.width = Math.max(20, original.width + scaled_delta.x);
|
|
441
|
+
break;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
// Apply constraints
|
|
445
|
+
this.constrain_crop_bounds();
|
|
446
|
+
// Update the UI and mask
|
|
447
|
+
this.update_crop_ui();
|
|
448
|
+
this.update_crop_mask();
|
|
449
|
+
// Make sure to apply mask to the current background image
|
|
450
|
+
if (this.crop_mask && this.image_editor_context.background_image) {
|
|
451
|
+
this.image_editor_context.background_image.setMask(this.crop_mask);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* constrains the crop bounds to the image dimensions
|
|
456
|
+
*/
|
|
457
|
+
constrain_crop_bounds() {
|
|
458
|
+
// Store original values
|
|
459
|
+
const original = {
|
|
460
|
+
x: this.crop_bounds.x,
|
|
461
|
+
y: this.crop_bounds.y,
|
|
462
|
+
width: this.crop_bounds.width,
|
|
463
|
+
height: this.crop_bounds.height
|
|
464
|
+
};
|
|
465
|
+
// First, constrain width and height to prevent exceeding image dimensions
|
|
466
|
+
// and ensure minimum size
|
|
467
|
+
this.crop_bounds.width = Math.max(20, Math.min(this.crop_bounds.width, this.dimensions.width));
|
|
468
|
+
this.crop_bounds.height = Math.max(20, Math.min(this.crop_bounds.height, this.dimensions.height));
|
|
469
|
+
// Then constrain x and y to keep the crop window within the image bounds
|
|
470
|
+
const oldX = this.crop_bounds.x;
|
|
471
|
+
this.crop_bounds.x = Math.max(0, Math.min(this.crop_bounds.x, this.dimensions.width - this.crop_bounds.width));
|
|
472
|
+
// If x position was constrained, adjust width to prevent opposite handle drift
|
|
473
|
+
if ((this.crop_bounds.x !== oldX && this.active_corner_index === 0) ||
|
|
474
|
+
this.active_edge_index === 2) {
|
|
475
|
+
// Left side was constrained, adjust width to compensate
|
|
476
|
+
const xDiff = this.crop_bounds.x - oldX;
|
|
477
|
+
this.crop_bounds.width -= xDiff;
|
|
478
|
+
}
|
|
479
|
+
const oldY = this.crop_bounds.y;
|
|
480
|
+
this.crop_bounds.y = Math.max(0, Math.min(this.crop_bounds.y, this.dimensions.height - this.crop_bounds.height));
|
|
481
|
+
// If y position was constrained, adjust height to prevent opposite handle drift
|
|
482
|
+
if (this.crop_bounds.y !== oldY &&
|
|
483
|
+
(this.active_corner_index === 0 ||
|
|
484
|
+
this.active_corner_index === 1 ||
|
|
485
|
+
this.active_edge_index === 0)) {
|
|
486
|
+
// Top side was constrained, adjust height to compensate
|
|
487
|
+
const yDiff = this.crop_bounds.y - oldY;
|
|
488
|
+
this.crop_bounds.height -= yDiff;
|
|
489
|
+
}
|
|
490
|
+
// Final check to ensure width and height don't exceed image dimensions from current position
|
|
491
|
+
this.crop_bounds.width = Math.max(20, Math.min(this.crop_bounds.width, this.dimensions.width - this.crop_bounds.x));
|
|
492
|
+
this.crop_bounds.height = Math.max(20, Math.min(this.crop_bounds.height, this.dimensions.height - this.crop_bounds.y));
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* updates the crop mask graphics
|
|
496
|
+
*/
|
|
497
|
+
update_crop_mask() {
|
|
498
|
+
if (!this.crop_mask)
|
|
499
|
+
return;
|
|
500
|
+
// Clear the previous mask shape
|
|
501
|
+
this.crop_mask.clear();
|
|
502
|
+
const { width, height } = this.image_editor_context.background_image?.getLocalBounds() ?? {
|
|
503
|
+
width: 0,
|
|
504
|
+
height: 0
|
|
505
|
+
};
|
|
506
|
+
this.crop_mask
|
|
507
|
+
.rect(0, 0, width, height)
|
|
508
|
+
.fill({ color: 0x000000, alpha: 0.4 })
|
|
509
|
+
.rect(this.crop_bounds.x, this.crop_bounds.y, this.crop_bounds.width, this.crop_bounds.height)
|
|
510
|
+
.cut();
|
|
511
|
+
// Reset blend mode
|
|
512
|
+
// this.crop_mask.blendMode = "normal";
|
|
513
|
+
// Make the mask visible (previously we had alpha=0)
|
|
514
|
+
// this.crop_mask.alpha = 1;
|
|
515
|
+
// Since we're not using this as a mask anymore, but as an overlay,
|
|
516
|
+
// we should remove any existing mask assignment
|
|
517
|
+
if (this.image_editor_context.background_image) {
|
|
518
|
+
this.image_editor_context.background_image.mask = null;
|
|
519
|
+
}
|
|
520
|
+
// Make sure the mask is not applied to the image container
|
|
521
|
+
if (this.image_editor_context.image_container.mask === this.crop_mask) {
|
|
522
|
+
this.image_editor_context.image_container.mask = null;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* updates the crop ui position and dimensions
|
|
527
|
+
*/
|
|
528
|
+
update_crop_ui() {
|
|
529
|
+
if (!this.crop_mask || !this.crop_ui_container)
|
|
530
|
+
return;
|
|
531
|
+
// Move crop_ui_container so that (0,0) of its local space aligns with the crop_bounds on the stage
|
|
532
|
+
this.crop_ui_container.position.set(this.position.x + this.crop_bounds.x * this.scale, this.position.y + this.crop_bounds.y * this.scale);
|
|
533
|
+
// Get the scaled dimensions
|
|
534
|
+
const scaled_width = this.crop_bounds.width * this.scale;
|
|
535
|
+
const scaled_height = this.crop_bounds.height * this.scale;
|
|
536
|
+
// Redraw the outline so it matches the crop_bounds size
|
|
537
|
+
const outline = this.crop_ui_container.getChildAt(0);
|
|
538
|
+
outline
|
|
539
|
+
.clear()
|
|
540
|
+
.rect(0, 0, scaled_width, scaled_height)
|
|
541
|
+
.stroke({ width: 1, color: 0x000000, alignment: 0, alpha: 0.5 });
|
|
542
|
+
// Update the hit area to match the new dimensions
|
|
543
|
+
outline.hitArea = new Rectangle(0, 0, scaled_width, scaled_height);
|
|
544
|
+
// Update handle positions to reflect the cropped area, not the full image
|
|
545
|
+
this.update_handle_positions(scaled_width, scaled_height);
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* updates the positions of all handles
|
|
549
|
+
* @param width - width of the crop area
|
|
550
|
+
* @param height - height of the crop area
|
|
551
|
+
*/
|
|
552
|
+
update_handle_positions(width, height) {
|
|
553
|
+
if (!this.crop_ui_container)
|
|
554
|
+
return;
|
|
555
|
+
// Skip the outline (index 0), get corner/edge handles
|
|
556
|
+
const handles = this.crop_ui_container.children.slice(1);
|
|
557
|
+
// First 4 are corners
|
|
558
|
+
const corners = handles.slice(0, 4);
|
|
559
|
+
const cornerPositions = [
|
|
560
|
+
{ x: 0, y: 0 }, // Top-left
|
|
561
|
+
{ x: width, y: 0 }, // Top-right
|
|
562
|
+
{ x: 0, y: height }, // Bottom-left
|
|
563
|
+
{ x: width, y: height } // Bottom-right
|
|
564
|
+
];
|
|
565
|
+
corners.forEach((handle, i) => {
|
|
566
|
+
const xScale = i % 2 === 0 ? 1 : -1;
|
|
567
|
+
const yScale = i < 2 ? 1 : -1;
|
|
568
|
+
handle.position.set(cornerPositions[i].x -
|
|
569
|
+
(xScale < 0 ? -this.LINE_THICKNESS : this.LINE_THICKNESS), cornerPositions[i].y -
|
|
570
|
+
(yScale < 0 ? -this.LINE_THICKNESS : this.LINE_THICKNESS));
|
|
571
|
+
});
|
|
572
|
+
// Remaining 4 are edges
|
|
573
|
+
const edges = handles.slice(4);
|
|
574
|
+
edges.forEach((handle, i) => {
|
|
575
|
+
if (i < 2) {
|
|
576
|
+
// Top/Bottom edges
|
|
577
|
+
handle.position.set(width / 2 - (this.CORNER_SIZE * 1.5) / 2, // Changed from 3 to 2
|
|
578
|
+
i === 0 ? -this.LINE_THICKNESS : height);
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
// Left/Right edges
|
|
582
|
+
handle.position.set(i === 2 ? 0 : width + this.LINE_THICKNESS, height / 2 - (this.CORNER_SIZE * 1.5) / 2 // Changed from 3 to 2
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* sets up event listeners for the crop tool
|
|
589
|
+
*/
|
|
590
|
+
setup_event_listeners() {
|
|
591
|
+
const stage = this.image_editor_context.app.stage;
|
|
592
|
+
stage.eventMode = "static";
|
|
593
|
+
stage.on("pointermove", this.handle_pointer_move.bind(this));
|
|
594
|
+
stage.on("pointerup", this.handle_pointer_up.bind(this));
|
|
595
|
+
stage.on("pointerupoutside", this.handle_pointer_up.bind(this));
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* removes event listeners for the crop tool
|
|
599
|
+
*/
|
|
600
|
+
cleanup_event_listeners() {
|
|
601
|
+
const stage = this.image_editor_context.app.stage;
|
|
602
|
+
stage.off("pointermove", this.handle_pointer_move.bind(this));
|
|
603
|
+
stage.off("pointerup", this.handle_pointer_up.bind(this));
|
|
604
|
+
stage.off("pointerupoutside", this.handle_pointer_up.bind(this));
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* handles pointer move events
|
|
608
|
+
* @param event - the pointer event
|
|
609
|
+
*/
|
|
610
|
+
handle_pointer_move(event) {
|
|
611
|
+
if (this.current_subtool !== "crop")
|
|
612
|
+
return;
|
|
613
|
+
if (this.is_dragging_window &&
|
|
614
|
+
this.drag_start_position &&
|
|
615
|
+
this.drag_start_bounds) {
|
|
616
|
+
// Handle window dragging
|
|
617
|
+
const local_pos = this.image_editor_context.image_container.toLocal(event.global);
|
|
618
|
+
const delta = new Point(local_pos.x - this.drag_start_position.x, local_pos.y - this.drag_start_position.y);
|
|
619
|
+
// Update crop bounds based on drag
|
|
620
|
+
this.crop_bounds = {
|
|
621
|
+
x: this.drag_start_bounds.x + delta.x,
|
|
622
|
+
y: this.drag_start_bounds.y + delta.y,
|
|
623
|
+
width: this.drag_start_bounds.width,
|
|
624
|
+
height: this.drag_start_bounds.height
|
|
625
|
+
};
|
|
626
|
+
// Constrain the bounds
|
|
627
|
+
this.constrain_crop_bounds();
|
|
628
|
+
// Update the mask
|
|
629
|
+
this.update_crop_mask();
|
|
630
|
+
// Make sure to apply mask to the current background image
|
|
631
|
+
if (this.crop_mask && this.image_editor_context.background_image) {
|
|
632
|
+
this.image_editor_context.background_image.setMask(this.crop_mask);
|
|
633
|
+
}
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
// Handle existing resize logic
|
|
637
|
+
if (this.is_dragging &&
|
|
638
|
+
this.selected_handle &&
|
|
639
|
+
this.last_pointer_position) {
|
|
640
|
+
// Convert from global to local, no extra divide
|
|
641
|
+
const local_pos = this.image_editor_context.image_container.toLocal(event.global);
|
|
642
|
+
const current_position = new Point(local_pos.x, local_pos.y);
|
|
643
|
+
const delta = new Point(current_position.x - this.last_pointer_position.x, current_position.y - this.last_pointer_position.y);
|
|
644
|
+
this.update_crop_bounds(delta);
|
|
645
|
+
this.last_pointer_position = current_position;
|
|
646
|
+
this.update_crop_mask();
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* handles pointer up events
|
|
651
|
+
*/
|
|
652
|
+
handle_pointer_up() {
|
|
653
|
+
if (this.current_subtool !== "crop")
|
|
654
|
+
return;
|
|
655
|
+
this.is_dragging = false;
|
|
656
|
+
this.is_dragging_window = false;
|
|
657
|
+
this.selected_handle = null;
|
|
658
|
+
this.last_pointer_position = null;
|
|
659
|
+
this.drag_start_position = null;
|
|
660
|
+
this.drag_start_bounds = null;
|
|
661
|
+
this.active_corner_index = -1;
|
|
662
|
+
this.active_edge_index = -1;
|
|
663
|
+
this.notify("change");
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* handles the start of window dragging
|
|
667
|
+
* @param event - the pointer event
|
|
668
|
+
*/
|
|
669
|
+
handle_window_drag_start(event) {
|
|
670
|
+
if (this.current_subtool !== "crop")
|
|
671
|
+
return;
|
|
672
|
+
event.stopPropagation();
|
|
673
|
+
this.is_dragging_window = true;
|
|
674
|
+
// Set flag indicating the crop has been manually changed
|
|
675
|
+
this.has_been_manually_changed = true;
|
|
676
|
+
// Store the initial position and bounds
|
|
677
|
+
const local_pos = this.image_editor_context.image_container.toLocal(event.global);
|
|
678
|
+
this.drag_start_position = new Point(local_pos.x, local_pos.y);
|
|
679
|
+
this.drag_start_bounds = { ...this.crop_bounds };
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* @returns whether the crop has been manually changed by the user
|
|
683
|
+
*/
|
|
684
|
+
get crop_manually_changed() {
|
|
685
|
+
return this.has_been_manually_changed;
|
|
686
|
+
}
|
|
687
|
+
on(event, callback) {
|
|
688
|
+
this.event_callbacks.set(event, [
|
|
689
|
+
...(this.event_callbacks.get(event) || []),
|
|
690
|
+
callback
|
|
691
|
+
]);
|
|
692
|
+
}
|
|
693
|
+
off(event, callback) {
|
|
694
|
+
this.event_callbacks.set(event, this.event_callbacks.get(event)?.filter((cb) => cb !== callback) || []);
|
|
695
|
+
}
|
|
696
|
+
notify(event) {
|
|
697
|
+
for (const callback of this.event_callbacks.get(event) || []) {
|
|
698
|
+
callback();
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
get_crop_bounds() {
|
|
702
|
+
const { width, height } = this.image_editor_context.background_image?.getLocalBounds() ?? {
|
|
703
|
+
width: 0,
|
|
704
|
+
height: 0
|
|
705
|
+
};
|
|
706
|
+
return {
|
|
707
|
+
x: this.crop_bounds.x,
|
|
708
|
+
y: this.crop_bounds.y,
|
|
709
|
+
crop_dimensions: {
|
|
710
|
+
width: this.crop_bounds.width,
|
|
711
|
+
height: this.crop_bounds.height
|
|
712
|
+
},
|
|
713
|
+
image_dimensions: {
|
|
714
|
+
width,
|
|
715
|
+
height
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
get_image() {
|
|
720
|
+
if (!this.image_editor_context.background_image)
|
|
721
|
+
return Promise.resolve(null);
|
|
722
|
+
const container = new Container();
|
|
723
|
+
const sprite = new Sprite(this.image_editor_context.background_image.texture);
|
|
724
|
+
container.addChild(sprite);
|
|
725
|
+
return get_canvas_blob(this.image_editor_context.app.renderer, container, this.crop_bounds);
|
|
726
|
+
}
|
|
727
|
+
}
|