@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.
- package/CHANGELOG.md +14 -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 +10 -9
- 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,1027 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Container,
|
|
3
|
+
Graphics,
|
|
4
|
+
Point,
|
|
5
|
+
Rectangle,
|
|
6
|
+
FederatedPointerEvent,
|
|
7
|
+
Sprite
|
|
8
|
+
} from "pixi.js";
|
|
9
|
+
import { type ImageEditorContext, type Tool } from "../core/editor";
|
|
10
|
+
|
|
11
|
+
import { type Tool as ToolbarTool, type Subtool } from "../Toolbar.svelte";
|
|
12
|
+
import { get_canvas_blob } from "../utils/pixi";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @interface CropBounds
|
|
16
|
+
* @description Defines the bounds of a crop area
|
|
17
|
+
* @property {number} x - The x-coordinate of the crop area
|
|
18
|
+
* @property {number} y - The y-coordinate of the crop area
|
|
19
|
+
* @property {number} width - The width of the crop area
|
|
20
|
+
* @property {number} height - The height of the crop area
|
|
21
|
+
*/
|
|
22
|
+
interface CropBounds {
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
width: number;
|
|
26
|
+
height: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @interface EditorState
|
|
31
|
+
* @description Defines the state of the editor
|
|
32
|
+
* @property {ToolbarTool} current_tool - The currently selected tool
|
|
33
|
+
* @property {Subtool} current_subtool - The currently selected subtool
|
|
34
|
+
*/
|
|
35
|
+
interface EditorState {
|
|
36
|
+
current_tool: ToolbarTool;
|
|
37
|
+
current_subtool: Subtool;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @class CropTool
|
|
42
|
+
* @implements {Tool}
|
|
43
|
+
* @description Implements the crop tool functionality for the image editor
|
|
44
|
+
*/
|
|
45
|
+
export class CropTool implements Tool {
|
|
46
|
+
name = "crop" as const;
|
|
47
|
+
private image_editor_context!: ImageEditorContext;
|
|
48
|
+
current_tool: ToolbarTool = "image";
|
|
49
|
+
current_subtool: Subtool = "crop";
|
|
50
|
+
|
|
51
|
+
// Constants
|
|
52
|
+
private readonly CORNER_SIZE = 25;
|
|
53
|
+
private readonly LINE_THICKNESS = 5;
|
|
54
|
+
private readonly HANDLE_COLOR = 0x000000;
|
|
55
|
+
private readonly HIT_AREA_SIZE = 40;
|
|
56
|
+
|
|
57
|
+
// State
|
|
58
|
+
private is_dragging = false;
|
|
59
|
+
private selected_handle: Container | null = null;
|
|
60
|
+
private active_corner_index = -1;
|
|
61
|
+
private active_edge_index = -1;
|
|
62
|
+
private last_pointer_position: Point | null = null;
|
|
63
|
+
crop_bounds: CropBounds = { x: 0, y: 0, width: 0, height: 0 };
|
|
64
|
+
private crop_ui_container: Container | null = null;
|
|
65
|
+
private crop_mask: Graphics | null = null;
|
|
66
|
+
private dimensions: { width: number; height: number } = {
|
|
67
|
+
width: 0,
|
|
68
|
+
height: 0
|
|
69
|
+
};
|
|
70
|
+
private position: { x: number; y: number } = { x: 0, y: 0 };
|
|
71
|
+
private scale = 1;
|
|
72
|
+
private is_dragging_window = false;
|
|
73
|
+
private drag_start_position: Point | null = null;
|
|
74
|
+
private drag_start_bounds: CropBounds | null = null;
|
|
75
|
+
private background_image_watcher: (() => void) | null = null;
|
|
76
|
+
private has_been_manually_changed = false;
|
|
77
|
+
private event_callbacks: Map<string, (() => void)[]> = new Map();
|
|
78
|
+
/**
|
|
79
|
+
* @method setup
|
|
80
|
+
* @async
|
|
81
|
+
* @description Sets up the crop tool with the given context and state
|
|
82
|
+
* @param {ImageEditorContext} context - The image editor context
|
|
83
|
+
* @param {ToolbarTool} tool - The current tool
|
|
84
|
+
* @param {Subtool} subtool - The current subtool
|
|
85
|
+
* @returns {Promise<void>}
|
|
86
|
+
*/
|
|
87
|
+
async setup(
|
|
88
|
+
context: ImageEditorContext,
|
|
89
|
+
tool: ToolbarTool,
|
|
90
|
+
subtool: Subtool
|
|
91
|
+
): Promise<void> {
|
|
92
|
+
this.image_editor_context = context;
|
|
93
|
+
this.current_tool = tool;
|
|
94
|
+
this.current_subtool = subtool;
|
|
95
|
+
|
|
96
|
+
context.dimensions.subscribe((dimensions) => {
|
|
97
|
+
this.dimensions = dimensions;
|
|
98
|
+
|
|
99
|
+
// Update crop bounds to match new dimensions
|
|
100
|
+
this.crop_bounds = {
|
|
101
|
+
x: 0,
|
|
102
|
+
y: 0,
|
|
103
|
+
width: dimensions.width,
|
|
104
|
+
height: dimensions.height
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Force UI update if it exists
|
|
108
|
+
if (this.crop_ui_container) {
|
|
109
|
+
this.update_crop_ui();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Always update the mask when dimensions change
|
|
113
|
+
this.set_crop_mask();
|
|
114
|
+
this.update_crop_mask();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
context.position.subscribe((position) => {
|
|
118
|
+
this.position = position;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
context.scale.subscribe((scale) => {
|
|
122
|
+
this.scale = scale;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Setup a background image watcher to ensure we always apply the mask to the current background image
|
|
126
|
+
this.background_image_watcher = () => {
|
|
127
|
+
// Only apply when crop tool is active and we have a mask
|
|
128
|
+
if (
|
|
129
|
+
this.crop_mask &&
|
|
130
|
+
this.current_tool === "image" &&
|
|
131
|
+
this.current_subtool === "crop"
|
|
132
|
+
) {
|
|
133
|
+
// Get a fresh reference to the background image and apply the mask
|
|
134
|
+
if (this.image_editor_context.background_image) {
|
|
135
|
+
this.image_editor_context.background_image.setMask(this.crop_mask);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// Add the watcher to the ticker to regularly check for background image changes
|
|
141
|
+
this.image_editor_context.app.ticker.add(this.background_image_watcher);
|
|
142
|
+
|
|
143
|
+
// Initialize crop UI
|
|
144
|
+
await this.init_crop_ui();
|
|
145
|
+
|
|
146
|
+
// Setup event listeners
|
|
147
|
+
this.setup_event_listeners();
|
|
148
|
+
|
|
149
|
+
// Ensure the crop UI visibility matches the current tool state
|
|
150
|
+
const isCropActive = tool === "image" && subtool === "crop";
|
|
151
|
+
if (this.crop_ui_container) {
|
|
152
|
+
this.crop_ui_container.visible = isCropActive;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @method cleanup
|
|
158
|
+
* @description Cleans up the crop tool resources and removes event listeners
|
|
159
|
+
* @returns {void}
|
|
160
|
+
*/
|
|
161
|
+
cleanup(): void {
|
|
162
|
+
// Remove the crop UI container
|
|
163
|
+
if (this.crop_ui_container) {
|
|
164
|
+
this.image_editor_context.app.stage.removeChild(this.crop_ui_container);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Remove the mask and reset the image container's mask property
|
|
168
|
+
if (this.crop_mask) {
|
|
169
|
+
if (this.crop_mask.parent) {
|
|
170
|
+
this.crop_mask.parent.removeChild(this.crop_mask);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Clear mask from background_image if applied
|
|
174
|
+
if (
|
|
175
|
+
this.image_editor_context.background_image &&
|
|
176
|
+
this.image_editor_context.background_image.mask === this.crop_mask
|
|
177
|
+
) {
|
|
178
|
+
this.image_editor_context.background_image.mask = null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Clear mask from image_container if applied
|
|
182
|
+
if (this.image_editor_context.image_container.mask === this.crop_mask) {
|
|
183
|
+
this.image_editor_context.image_container.mask = null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Ensure all references to the crop mask are cleared
|
|
188
|
+
this.crop_mask = null;
|
|
189
|
+
this.crop_ui_container = null;
|
|
190
|
+
|
|
191
|
+
// Remove the background image watcher from the ticker
|
|
192
|
+
if (this.background_image_watcher) {
|
|
193
|
+
this.image_editor_context.app.ticker.remove(
|
|
194
|
+
this.background_image_watcher
|
|
195
|
+
);
|
|
196
|
+
this.background_image_watcher = null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Remove event listeners
|
|
200
|
+
this.cleanup_event_listeners();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* @method set_tool
|
|
205
|
+
* @description Sets the current tool and subtool, updating UI visibility
|
|
206
|
+
* @param {ToolbarTool} tool - The tool to set
|
|
207
|
+
* @param {Subtool} subtool - The subtool to set
|
|
208
|
+
* @returns {void}
|
|
209
|
+
*/
|
|
210
|
+
set_tool(tool: ToolbarTool, subtool: Subtool): void {
|
|
211
|
+
this.current_tool = tool;
|
|
212
|
+
this.current_subtool = subtool;
|
|
213
|
+
|
|
214
|
+
// Show/hide crop UI based on tool and subtool
|
|
215
|
+
const isCropActive = tool === "image" && subtool === "crop";
|
|
216
|
+
|
|
217
|
+
// Only update the UI visibility, not the mask
|
|
218
|
+
if (this.crop_ui_container) {
|
|
219
|
+
this.crop_ui_container.visible = isCropActive;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// If crop tool is active, ensure the mask is up to date
|
|
223
|
+
if (isCropActive && this.crop_mask) {
|
|
224
|
+
this.update_crop_mask();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* @method set_crop_mask
|
|
230
|
+
* @private
|
|
231
|
+
* @description Sets up the crop mask for the image
|
|
232
|
+
* @returns {void}
|
|
233
|
+
*/
|
|
234
|
+
private set_crop_mask(): void {
|
|
235
|
+
// Remove any existing mask to avoid duplicates
|
|
236
|
+
if (this.crop_mask) {
|
|
237
|
+
if (this.crop_mask.parent) {
|
|
238
|
+
this.crop_mask.parent.removeChild(this.crop_mask);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Clear mask from image_container if it was applied there
|
|
242
|
+
if (this.image_editor_context.image_container.mask === this.crop_mask) {
|
|
243
|
+
this.image_editor_context.image_container.mask = null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Clear mask from background_image if it was applied there
|
|
247
|
+
if (
|
|
248
|
+
this.image_editor_context.background_image &&
|
|
249
|
+
this.image_editor_context.background_image.mask === this.crop_mask
|
|
250
|
+
) {
|
|
251
|
+
this.image_editor_context.background_image.mask = null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Create a new mask that will actually be used as an overlay
|
|
256
|
+
this.crop_mask = new Graphics();
|
|
257
|
+
|
|
258
|
+
// Add the mask/overlay to the display list
|
|
259
|
+
// This time it should be visible since we want to see the semi-transparent overlay
|
|
260
|
+
this.image_editor_context.image_container.addChild(this.crop_mask);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* @method init_crop_ui
|
|
265
|
+
* @private
|
|
266
|
+
* @async
|
|
267
|
+
* @description Initializes the crop UI elements and sets up the update loop
|
|
268
|
+
* @returns {Promise<void>}
|
|
269
|
+
*/
|
|
270
|
+
private async init_crop_ui(): Promise<void> {
|
|
271
|
+
// Create crop UI with scaled dimensions
|
|
272
|
+
const { width, height } =
|
|
273
|
+
this.image_editor_context.background_image?.getLocalBounds() || {
|
|
274
|
+
width: false,
|
|
275
|
+
height: false
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
this.crop_ui_container = this.make_crop_ui(
|
|
279
|
+
this.dimensions.width * this.scale,
|
|
280
|
+
this.dimensions.height * this.scale
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
// Position the crop UI container at the image position
|
|
284
|
+
this.crop_ui_container.position.set(this.position.x, this.position.y);
|
|
285
|
+
|
|
286
|
+
// Initially hide the crop UI until explicitly enabled by set_tool
|
|
287
|
+
this.crop_ui_container.visible = false;
|
|
288
|
+
|
|
289
|
+
this.image_editor_context.app.stage.addChild(this.crop_ui_container);
|
|
290
|
+
|
|
291
|
+
// Create and apply mask to the image container
|
|
292
|
+
this.set_crop_mask();
|
|
293
|
+
|
|
294
|
+
// Start the update loop for the UI
|
|
295
|
+
this.image_editor_context.app.ticker.add(this.update_crop_ui.bind(this));
|
|
296
|
+
|
|
297
|
+
// Initialize the mask with the current crop bounds
|
|
298
|
+
this.update_crop_mask();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* @method make_crop_ui
|
|
303
|
+
* @private
|
|
304
|
+
* @description Creates the crop UI with the specified dimensions
|
|
305
|
+
* @param {number} width - Width of the crop area
|
|
306
|
+
* @param {number} height - Height of the crop area
|
|
307
|
+
* @returns {Container} The created crop UI container
|
|
308
|
+
*/
|
|
309
|
+
private make_crop_ui(width: number, height: number): Container {
|
|
310
|
+
const crop_container = new Container();
|
|
311
|
+
crop_container.eventMode = "static";
|
|
312
|
+
crop_container.interactiveChildren = true;
|
|
313
|
+
|
|
314
|
+
// Create crop outline with hit area for dragging
|
|
315
|
+
const crop_outline = new Graphics()
|
|
316
|
+
.rect(0, 0, width, height)
|
|
317
|
+
.stroke({ width: 1, color: 0x000000, alignment: 0, alpha: 0.5 });
|
|
318
|
+
|
|
319
|
+
// Make the outline interactive
|
|
320
|
+
crop_outline.eventMode = "static";
|
|
321
|
+
crop_outline.cursor = "move";
|
|
322
|
+
|
|
323
|
+
// Set hit area to match the outline's dimensions
|
|
324
|
+
crop_outline.hitArea = new Rectangle(0, 0, width, height);
|
|
325
|
+
|
|
326
|
+
// Add drag handlers to the outline
|
|
327
|
+
crop_outline.on("pointerdown", this.handle_window_drag_start.bind(this));
|
|
328
|
+
|
|
329
|
+
crop_container.addChild(crop_outline);
|
|
330
|
+
|
|
331
|
+
// Create corner handles
|
|
332
|
+
this.create_corner_handles(crop_container, width, height);
|
|
333
|
+
|
|
334
|
+
// Create edge handles
|
|
335
|
+
this.create_edge_handles(crop_container, width, height);
|
|
336
|
+
|
|
337
|
+
return crop_container;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* @method create_handle
|
|
342
|
+
* @private
|
|
343
|
+
* @description Creates a handle for the crop UI
|
|
344
|
+
* @param {boolean} [is_edge=false] - Whether the handle is an edge handle
|
|
345
|
+
* @returns {Container} The created handle container
|
|
346
|
+
*/
|
|
347
|
+
private create_handle(is_edge = false): Container {
|
|
348
|
+
const handle = new Container();
|
|
349
|
+
handle.eventMode = "static";
|
|
350
|
+
|
|
351
|
+
const handle_graphics = new Graphics();
|
|
352
|
+
|
|
353
|
+
if (is_edge) {
|
|
354
|
+
// Create a shorter bar for edge handles (2x instead of 3x)
|
|
355
|
+
handle_graphics
|
|
356
|
+
.rect(0, 0, this.CORNER_SIZE * 1.5, this.LINE_THICKNESS)
|
|
357
|
+
.fill(this.HANDLE_COLOR);
|
|
358
|
+
} else {
|
|
359
|
+
// Create L-shaped corner handles
|
|
360
|
+
handle_graphics
|
|
361
|
+
.rect(0, 0, this.CORNER_SIZE, this.LINE_THICKNESS)
|
|
362
|
+
.rect(0, 0, this.LINE_THICKNESS, this.CORNER_SIZE)
|
|
363
|
+
.fill(this.HANDLE_COLOR);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
handle.addChild(handle_graphics);
|
|
367
|
+
|
|
368
|
+
// Adjust hit area based on handle type
|
|
369
|
+
const hit_size = is_edge ? this.HIT_AREA_SIZE * 1.5 : this.HIT_AREA_SIZE;
|
|
370
|
+
handle.hitArea = new Rectangle(
|
|
371
|
+
-hit_size / 2 + this.LINE_THICKNESS,
|
|
372
|
+
-hit_size / 2 + this.LINE_THICKNESS,
|
|
373
|
+
hit_size,
|
|
374
|
+
hit_size
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
return handle;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* @method create_edge_handles
|
|
382
|
+
* @private
|
|
383
|
+
* @description Creates edge handles for the crop UI
|
|
384
|
+
* @param {Container} container - The container to add handles to
|
|
385
|
+
* @param {number} width - Width of the crop area
|
|
386
|
+
* @param {number} height - Height of the crop area
|
|
387
|
+
* @returns {void}
|
|
388
|
+
*/
|
|
389
|
+
private create_edge_handles(
|
|
390
|
+
container: Container,
|
|
391
|
+
width: number,
|
|
392
|
+
height: number
|
|
393
|
+
): void {
|
|
394
|
+
// Horizontal handles at top and bottom
|
|
395
|
+
[-1, 1].forEach((i, index) => {
|
|
396
|
+
const handle = this.create_handle(true); // true for edge handle
|
|
397
|
+
handle.rotation = 0;
|
|
398
|
+
|
|
399
|
+
handle.on("pointerdown", (event: FederatedPointerEvent) => {
|
|
400
|
+
this.handle_pointer_down(event, handle, -1, index); // 0 for top, 1 for bottom
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Center the handle
|
|
404
|
+
handle.x = width / 2 - (this.CORNER_SIZE * 3) / 2;
|
|
405
|
+
handle.y = i < 0 ? -this.LINE_THICKNESS : height;
|
|
406
|
+
handle.cursor = "ns-resize";
|
|
407
|
+
|
|
408
|
+
container.addChild(handle);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Vertical handles at left and right
|
|
412
|
+
[-1, 1].forEach((i, index) => {
|
|
413
|
+
const handle = this.create_handle(true); // true for edge handle
|
|
414
|
+
handle.rotation = Math.PI / 2; // Rotate 90 degrees
|
|
415
|
+
|
|
416
|
+
handle.on("pointerdown", (event: FederatedPointerEvent) => {
|
|
417
|
+
this.handle_pointer_down(event, handle, -1, index + 2); // 2 for left, 3 for right
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// Center the handle
|
|
421
|
+
handle.x = i < 0 ? -this.LINE_THICKNESS : width;
|
|
422
|
+
handle.y = height / 2 - (this.CORNER_SIZE * 3) / 2;
|
|
423
|
+
handle.cursor = "ew-resize";
|
|
424
|
+
|
|
425
|
+
container.addChild(handle);
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* @method create_corner_handles
|
|
431
|
+
* @private
|
|
432
|
+
* @description Creates corner handles for the crop UI
|
|
433
|
+
* @param {Container} container - The container to add handles to
|
|
434
|
+
* @param {number} width - Width of the crop area
|
|
435
|
+
* @param {number} height - Height of the crop area
|
|
436
|
+
* @returns {void}
|
|
437
|
+
*/
|
|
438
|
+
private create_corner_handles(
|
|
439
|
+
container: Container,
|
|
440
|
+
width: number,
|
|
441
|
+
height: number
|
|
442
|
+
): void {
|
|
443
|
+
const corners = [
|
|
444
|
+
{ x: 0, y: 0, xScale: 1, yScale: 1, cursor: "nwse-resize" }, // Top-left
|
|
445
|
+
{ x: width, y: 0, xScale: -1, yScale: 1, cursor: "nesw-resize" }, // Top-right
|
|
446
|
+
{ x: 0, y: height, xScale: 1, yScale: -1, cursor: "nesw-resize" }, // Bottom-left
|
|
447
|
+
{ x: width, y: height, xScale: -1, yScale: -1, cursor: "nwse-resize" } // Bottom-right
|
|
448
|
+
];
|
|
449
|
+
|
|
450
|
+
corners.forEach(({ x, y, xScale, yScale, cursor }, i) => {
|
|
451
|
+
const corner = this.create_handle(false); // false for corner handle
|
|
452
|
+
corner.x = x - (xScale < 0 ? -this.LINE_THICKNESS : this.LINE_THICKNESS);
|
|
453
|
+
corner.y = y - (yScale < 0 ? -this.LINE_THICKNESS : this.LINE_THICKNESS);
|
|
454
|
+
corner.scale.set(xScale, yScale);
|
|
455
|
+
|
|
456
|
+
corner.on("pointerdown", (event: FederatedPointerEvent) => {
|
|
457
|
+
this.handle_pointer_down(event, corner, i, -1);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
corner.cursor = cursor;
|
|
461
|
+
|
|
462
|
+
container.addChild(corner);
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* @method handle_pointer_down
|
|
468
|
+
* @private
|
|
469
|
+
* @description Handles pointer down events on handles
|
|
470
|
+
* @param {FederatedPointerEvent} event - The pointer event
|
|
471
|
+
* @param {Container} handle - The handle that was clicked
|
|
472
|
+
* @param {number} corner_index - Index of the corner (-1 if not a corner)
|
|
473
|
+
* @param {number} edge_index - Index of the edge (-1 if not an edge)
|
|
474
|
+
* @returns {void}
|
|
475
|
+
*/
|
|
476
|
+
private handle_pointer_down(
|
|
477
|
+
event: FederatedPointerEvent,
|
|
478
|
+
handle: Container,
|
|
479
|
+
corner_index: number,
|
|
480
|
+
edge_index: number
|
|
481
|
+
): void {
|
|
482
|
+
// Only handle crop events if crop tool is active
|
|
483
|
+
if (this.current_subtool !== "crop") return;
|
|
484
|
+
|
|
485
|
+
event.stopPropagation();
|
|
486
|
+
|
|
487
|
+
this.is_dragging = true;
|
|
488
|
+
this.selected_handle = handle;
|
|
489
|
+
this.active_corner_index = corner_index;
|
|
490
|
+
this.active_edge_index = edge_index;
|
|
491
|
+
|
|
492
|
+
// Convert global position to the container's local coordinates.
|
|
493
|
+
const local_pos = this.image_editor_context.image_container.toLocal(
|
|
494
|
+
event.global
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
this.last_pointer_position = new Point(local_pos.x, local_pos.y);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* updates the crop bounds based on pointer movement
|
|
502
|
+
* @param delta - the change in position
|
|
503
|
+
*/
|
|
504
|
+
private update_crop_bounds(delta: Point): void {
|
|
505
|
+
// Set flag indicating the crop has been manually changed
|
|
506
|
+
this.has_been_manually_changed = true;
|
|
507
|
+
|
|
508
|
+
// Scale the delta to match the image coordinates
|
|
509
|
+
const scaled_delta = new Point(delta.x, delta.y);
|
|
510
|
+
|
|
511
|
+
// Store original values to calculate actual changes
|
|
512
|
+
const original = {
|
|
513
|
+
x: this.crop_bounds.x,
|
|
514
|
+
y: this.crop_bounds.y,
|
|
515
|
+
width: this.crop_bounds.width,
|
|
516
|
+
height: this.crop_bounds.height
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
if (this.active_corner_index !== -1) {
|
|
520
|
+
// Handle corner dragging
|
|
521
|
+
switch (this.active_corner_index) {
|
|
522
|
+
case 0: // Top-left
|
|
523
|
+
// Update width and x position
|
|
524
|
+
const newWidth0 = Math.max(20, original.width - scaled_delta.x);
|
|
525
|
+
const widthDiff0 = original.width - newWidth0;
|
|
526
|
+
this.crop_bounds.width = newWidth0;
|
|
527
|
+
this.crop_bounds.x = original.x + widthDiff0;
|
|
528
|
+
|
|
529
|
+
// Update height and y position
|
|
530
|
+
const newHeight0 = Math.max(20, original.height - scaled_delta.y);
|
|
531
|
+
const heightDiff0 = original.height - newHeight0;
|
|
532
|
+
this.crop_bounds.height = newHeight0;
|
|
533
|
+
this.crop_bounds.y = original.y + heightDiff0;
|
|
534
|
+
break;
|
|
535
|
+
|
|
536
|
+
case 1: // Top-right
|
|
537
|
+
// Update width (right side)
|
|
538
|
+
this.crop_bounds.width = Math.max(
|
|
539
|
+
20,
|
|
540
|
+
original.width + scaled_delta.x
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
// Update height and y position (top side)
|
|
544
|
+
const newHeight1 = Math.max(20, original.height - scaled_delta.y);
|
|
545
|
+
const heightDiff1 = original.height - newHeight1;
|
|
546
|
+
this.crop_bounds.height = newHeight1;
|
|
547
|
+
this.crop_bounds.y = original.y + heightDiff1;
|
|
548
|
+
break;
|
|
549
|
+
|
|
550
|
+
case 2: // Bottom-left
|
|
551
|
+
// Update width and x position (left side)
|
|
552
|
+
const newWidth2 = Math.max(20, original.width - scaled_delta.x);
|
|
553
|
+
const widthDiff2 = original.width - newWidth2;
|
|
554
|
+
this.crop_bounds.width = newWidth2;
|
|
555
|
+
this.crop_bounds.x = original.x + widthDiff2;
|
|
556
|
+
|
|
557
|
+
// Update height (bottom side)
|
|
558
|
+
this.crop_bounds.height = Math.max(
|
|
559
|
+
20,
|
|
560
|
+
original.height + scaled_delta.y
|
|
561
|
+
);
|
|
562
|
+
break;
|
|
563
|
+
|
|
564
|
+
case 3: // Bottom-right
|
|
565
|
+
// Update width (right side)
|
|
566
|
+
this.crop_bounds.width = Math.max(
|
|
567
|
+
20,
|
|
568
|
+
original.width + scaled_delta.x
|
|
569
|
+
);
|
|
570
|
+
|
|
571
|
+
// Update height (bottom side)
|
|
572
|
+
this.crop_bounds.height = Math.max(
|
|
573
|
+
20,
|
|
574
|
+
original.height + scaled_delta.y
|
|
575
|
+
);
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
} else if (this.active_edge_index !== -1) {
|
|
579
|
+
// Handle edge dragging
|
|
580
|
+
switch (this.active_edge_index) {
|
|
581
|
+
case 0: // Top
|
|
582
|
+
// Update height and y position
|
|
583
|
+
const newHeight = Math.max(20, original.height - scaled_delta.y);
|
|
584
|
+
const heightDiff = original.height - newHeight;
|
|
585
|
+
this.crop_bounds.height = newHeight;
|
|
586
|
+
this.crop_bounds.y = original.y + heightDiff;
|
|
587
|
+
break;
|
|
588
|
+
|
|
589
|
+
case 1: // Bottom
|
|
590
|
+
// Update height only
|
|
591
|
+
this.crop_bounds.height = Math.max(
|
|
592
|
+
20,
|
|
593
|
+
original.height + scaled_delta.y
|
|
594
|
+
);
|
|
595
|
+
break;
|
|
596
|
+
|
|
597
|
+
case 2: // Left
|
|
598
|
+
// Update width and x position
|
|
599
|
+
const newWidth = Math.max(20, original.width - scaled_delta.x);
|
|
600
|
+
const widthDiff = original.width - newWidth;
|
|
601
|
+
this.crop_bounds.width = newWidth;
|
|
602
|
+
this.crop_bounds.x = original.x + widthDiff;
|
|
603
|
+
break;
|
|
604
|
+
|
|
605
|
+
case 3: // Right
|
|
606
|
+
// Update width only
|
|
607
|
+
this.crop_bounds.width = Math.max(
|
|
608
|
+
20,
|
|
609
|
+
original.width + scaled_delta.x
|
|
610
|
+
);
|
|
611
|
+
break;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Apply constraints
|
|
616
|
+
this.constrain_crop_bounds();
|
|
617
|
+
|
|
618
|
+
// Update the UI and mask
|
|
619
|
+
this.update_crop_ui();
|
|
620
|
+
this.update_crop_mask();
|
|
621
|
+
|
|
622
|
+
// Make sure to apply mask to the current background image
|
|
623
|
+
if (this.crop_mask && this.image_editor_context.background_image) {
|
|
624
|
+
this.image_editor_context.background_image.setMask(this.crop_mask);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* constrains the crop bounds to the image dimensions
|
|
630
|
+
*/
|
|
631
|
+
private constrain_crop_bounds(): void {
|
|
632
|
+
// Store original values
|
|
633
|
+
const original = {
|
|
634
|
+
x: this.crop_bounds.x,
|
|
635
|
+
y: this.crop_bounds.y,
|
|
636
|
+
width: this.crop_bounds.width,
|
|
637
|
+
height: this.crop_bounds.height
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
// First, constrain width and height to prevent exceeding image dimensions
|
|
641
|
+
// and ensure minimum size
|
|
642
|
+
this.crop_bounds.width = Math.max(
|
|
643
|
+
20,
|
|
644
|
+
Math.min(this.crop_bounds.width, this.dimensions.width)
|
|
645
|
+
);
|
|
646
|
+
|
|
647
|
+
this.crop_bounds.height = Math.max(
|
|
648
|
+
20,
|
|
649
|
+
Math.min(this.crop_bounds.height, this.dimensions.height)
|
|
650
|
+
);
|
|
651
|
+
|
|
652
|
+
// Then constrain x and y to keep the crop window within the image bounds
|
|
653
|
+
const oldX = this.crop_bounds.x;
|
|
654
|
+
this.crop_bounds.x = Math.max(
|
|
655
|
+
0,
|
|
656
|
+
Math.min(
|
|
657
|
+
this.crop_bounds.x,
|
|
658
|
+
this.dimensions.width - this.crop_bounds.width
|
|
659
|
+
)
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
// If x position was constrained, adjust width to prevent opposite handle drift
|
|
663
|
+
if (
|
|
664
|
+
(this.crop_bounds.x !== oldX && this.active_corner_index === 0) ||
|
|
665
|
+
this.active_edge_index === 2
|
|
666
|
+
) {
|
|
667
|
+
// Left side was constrained, adjust width to compensate
|
|
668
|
+
const xDiff = this.crop_bounds.x - oldX;
|
|
669
|
+
this.crop_bounds.width -= xDiff;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const oldY = this.crop_bounds.y;
|
|
673
|
+
this.crop_bounds.y = Math.max(
|
|
674
|
+
0,
|
|
675
|
+
Math.min(
|
|
676
|
+
this.crop_bounds.y,
|
|
677
|
+
this.dimensions.height - this.crop_bounds.height
|
|
678
|
+
)
|
|
679
|
+
);
|
|
680
|
+
|
|
681
|
+
// If y position was constrained, adjust height to prevent opposite handle drift
|
|
682
|
+
if (
|
|
683
|
+
this.crop_bounds.y !== oldY &&
|
|
684
|
+
(this.active_corner_index === 0 ||
|
|
685
|
+
this.active_corner_index === 1 ||
|
|
686
|
+
this.active_edge_index === 0)
|
|
687
|
+
) {
|
|
688
|
+
// Top side was constrained, adjust height to compensate
|
|
689
|
+
const yDiff = this.crop_bounds.y - oldY;
|
|
690
|
+
this.crop_bounds.height -= yDiff;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Final check to ensure width and height don't exceed image dimensions from current position
|
|
694
|
+
this.crop_bounds.width = Math.max(
|
|
695
|
+
20,
|
|
696
|
+
Math.min(
|
|
697
|
+
this.crop_bounds.width,
|
|
698
|
+
this.dimensions.width - this.crop_bounds.x
|
|
699
|
+
)
|
|
700
|
+
);
|
|
701
|
+
|
|
702
|
+
this.crop_bounds.height = Math.max(
|
|
703
|
+
20,
|
|
704
|
+
Math.min(
|
|
705
|
+
this.crop_bounds.height,
|
|
706
|
+
this.dimensions.height - this.crop_bounds.y
|
|
707
|
+
)
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* updates the crop mask graphics
|
|
713
|
+
*/
|
|
714
|
+
private update_crop_mask(): void {
|
|
715
|
+
if (!this.crop_mask) return;
|
|
716
|
+
|
|
717
|
+
// Clear the previous mask shape
|
|
718
|
+
this.crop_mask.clear();
|
|
719
|
+
|
|
720
|
+
const { width, height } =
|
|
721
|
+
this.image_editor_context.background_image?.getLocalBounds() ?? {
|
|
722
|
+
width: 0,
|
|
723
|
+
height: 0
|
|
724
|
+
};
|
|
725
|
+
this.crop_mask
|
|
726
|
+
.rect(0, 0, width, height)
|
|
727
|
+
.fill({ color: 0x000000, alpha: 0.4 })
|
|
728
|
+
.rect(
|
|
729
|
+
this.crop_bounds.x,
|
|
730
|
+
this.crop_bounds.y,
|
|
731
|
+
this.crop_bounds.width,
|
|
732
|
+
this.crop_bounds.height
|
|
733
|
+
)
|
|
734
|
+
.cut();
|
|
735
|
+
|
|
736
|
+
// Reset blend mode
|
|
737
|
+
// this.crop_mask.blendMode = "normal";
|
|
738
|
+
|
|
739
|
+
// Make the mask visible (previously we had alpha=0)
|
|
740
|
+
// this.crop_mask.alpha = 1;
|
|
741
|
+
|
|
742
|
+
// Since we're not using this as a mask anymore, but as an overlay,
|
|
743
|
+
// we should remove any existing mask assignment
|
|
744
|
+
if (this.image_editor_context.background_image) {
|
|
745
|
+
this.image_editor_context.background_image.mask = null;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Make sure the mask is not applied to the image container
|
|
749
|
+
if (this.image_editor_context.image_container.mask === this.crop_mask) {
|
|
750
|
+
this.image_editor_context.image_container.mask = null;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* updates the crop ui position and dimensions
|
|
756
|
+
*/
|
|
757
|
+
private update_crop_ui(): void {
|
|
758
|
+
if (!this.crop_mask || !this.crop_ui_container) return;
|
|
759
|
+
|
|
760
|
+
// Move crop_ui_container so that (0,0) of its local space aligns with the crop_bounds on the stage
|
|
761
|
+
this.crop_ui_container.position.set(
|
|
762
|
+
this.position.x + this.crop_bounds.x * this.scale,
|
|
763
|
+
this.position.y + this.crop_bounds.y * this.scale
|
|
764
|
+
);
|
|
765
|
+
|
|
766
|
+
// Get the scaled dimensions
|
|
767
|
+
const scaled_width = this.crop_bounds.width * this.scale;
|
|
768
|
+
const scaled_height = this.crop_bounds.height * this.scale;
|
|
769
|
+
|
|
770
|
+
// Redraw the outline so it matches the crop_bounds size
|
|
771
|
+
const outline = this.crop_ui_container.getChildAt(0) as Graphics;
|
|
772
|
+
outline
|
|
773
|
+
.clear()
|
|
774
|
+
.rect(0, 0, scaled_width, scaled_height)
|
|
775
|
+
.stroke({ width: 1, color: 0x000000, alignment: 0, alpha: 0.5 });
|
|
776
|
+
|
|
777
|
+
// Update the hit area to match the new dimensions
|
|
778
|
+
outline.hitArea = new Rectangle(0, 0, scaled_width, scaled_height);
|
|
779
|
+
|
|
780
|
+
// Update handle positions to reflect the cropped area, not the full image
|
|
781
|
+
this.update_handle_positions(scaled_width, scaled_height);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* updates the positions of all handles
|
|
786
|
+
* @param width - width of the crop area
|
|
787
|
+
* @param height - height of the crop area
|
|
788
|
+
*/
|
|
789
|
+
private update_handle_positions(width: number, height: number): void {
|
|
790
|
+
if (!this.crop_ui_container) return;
|
|
791
|
+
|
|
792
|
+
// Skip the outline (index 0), get corner/edge handles
|
|
793
|
+
const handles = this.crop_ui_container.children.slice(1);
|
|
794
|
+
|
|
795
|
+
// First 4 are corners
|
|
796
|
+
const corners = handles.slice(0, 4);
|
|
797
|
+
const cornerPositions = [
|
|
798
|
+
{ x: 0, y: 0 }, // Top-left
|
|
799
|
+
{ x: width, y: 0 }, // Top-right
|
|
800
|
+
{ x: 0, y: height }, // Bottom-left
|
|
801
|
+
{ x: width, y: height } // Bottom-right
|
|
802
|
+
];
|
|
803
|
+
|
|
804
|
+
corners.forEach((handle, i) => {
|
|
805
|
+
const xScale = i % 2 === 0 ? 1 : -1;
|
|
806
|
+
const yScale = i < 2 ? 1 : -1;
|
|
807
|
+
handle.position.set(
|
|
808
|
+
cornerPositions[i].x -
|
|
809
|
+
(xScale < 0 ? -this.LINE_THICKNESS : this.LINE_THICKNESS),
|
|
810
|
+
cornerPositions[i].y -
|
|
811
|
+
(yScale < 0 ? -this.LINE_THICKNESS : this.LINE_THICKNESS)
|
|
812
|
+
);
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
// Remaining 4 are edges
|
|
816
|
+
const edges = handles.slice(4);
|
|
817
|
+
edges.forEach((handle, i) => {
|
|
818
|
+
if (i < 2) {
|
|
819
|
+
// Top/Bottom edges
|
|
820
|
+
handle.position.set(
|
|
821
|
+
width / 2 - (this.CORNER_SIZE * 1.5) / 2, // Changed from 3 to 2
|
|
822
|
+
i === 0 ? -this.LINE_THICKNESS : height
|
|
823
|
+
);
|
|
824
|
+
} else {
|
|
825
|
+
// Left/Right edges
|
|
826
|
+
handle.position.set(
|
|
827
|
+
i === 2 ? 0 : width + this.LINE_THICKNESS,
|
|
828
|
+
height / 2 - (this.CORNER_SIZE * 1.5) / 2 // Changed from 3 to 2
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* sets up event listeners for the crop tool
|
|
836
|
+
*/
|
|
837
|
+
private setup_event_listeners(): void {
|
|
838
|
+
const stage = this.image_editor_context.app.stage;
|
|
839
|
+
stage.eventMode = "static";
|
|
840
|
+
stage.on("pointermove", this.handle_pointer_move.bind(this));
|
|
841
|
+
stage.on("pointerup", this.handle_pointer_up.bind(this));
|
|
842
|
+
stage.on("pointerupoutside", this.handle_pointer_up.bind(this));
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* removes event listeners for the crop tool
|
|
847
|
+
*/
|
|
848
|
+
private cleanup_event_listeners(): void {
|
|
849
|
+
const stage = this.image_editor_context.app.stage;
|
|
850
|
+
|
|
851
|
+
stage.off("pointermove", this.handle_pointer_move.bind(this));
|
|
852
|
+
stage.off("pointerup", this.handle_pointer_up.bind(this));
|
|
853
|
+
stage.off("pointerupoutside", this.handle_pointer_up.bind(this));
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* handles pointer move events
|
|
858
|
+
* @param event - the pointer event
|
|
859
|
+
*/
|
|
860
|
+
private handle_pointer_move(event: FederatedPointerEvent): void {
|
|
861
|
+
if (this.current_subtool !== "crop") return;
|
|
862
|
+
|
|
863
|
+
if (
|
|
864
|
+
this.is_dragging_window &&
|
|
865
|
+
this.drag_start_position &&
|
|
866
|
+
this.drag_start_bounds
|
|
867
|
+
) {
|
|
868
|
+
// Handle window dragging
|
|
869
|
+
const local_pos = this.image_editor_context.image_container.toLocal(
|
|
870
|
+
event.global
|
|
871
|
+
);
|
|
872
|
+
const delta = new Point(
|
|
873
|
+
local_pos.x - this.drag_start_position.x,
|
|
874
|
+
local_pos.y - this.drag_start_position.y
|
|
875
|
+
);
|
|
876
|
+
|
|
877
|
+
// Update crop bounds based on drag
|
|
878
|
+
this.crop_bounds = {
|
|
879
|
+
x: this.drag_start_bounds.x + delta.x,
|
|
880
|
+
y: this.drag_start_bounds.y + delta.y,
|
|
881
|
+
width: this.drag_start_bounds.width,
|
|
882
|
+
height: this.drag_start_bounds.height
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
// Constrain the bounds
|
|
886
|
+
this.constrain_crop_bounds();
|
|
887
|
+
|
|
888
|
+
// Update the mask
|
|
889
|
+
this.update_crop_mask();
|
|
890
|
+
|
|
891
|
+
// Make sure to apply mask to the current background image
|
|
892
|
+
if (this.crop_mask && this.image_editor_context.background_image) {
|
|
893
|
+
this.image_editor_context.background_image.setMask(this.crop_mask);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Handle existing resize logic
|
|
900
|
+
if (
|
|
901
|
+
this.is_dragging &&
|
|
902
|
+
this.selected_handle &&
|
|
903
|
+
this.last_pointer_position
|
|
904
|
+
) {
|
|
905
|
+
// Convert from global to local, no extra divide
|
|
906
|
+
const local_pos = this.image_editor_context.image_container.toLocal(
|
|
907
|
+
event.global
|
|
908
|
+
);
|
|
909
|
+
const current_position = new Point(local_pos.x, local_pos.y);
|
|
910
|
+
|
|
911
|
+
const delta = new Point(
|
|
912
|
+
current_position.x - this.last_pointer_position.x,
|
|
913
|
+
current_position.y - this.last_pointer_position.y
|
|
914
|
+
);
|
|
915
|
+
|
|
916
|
+
this.update_crop_bounds(delta);
|
|
917
|
+
this.last_pointer_position = current_position;
|
|
918
|
+
this.update_crop_mask();
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* handles pointer up events
|
|
924
|
+
*/
|
|
925
|
+
private handle_pointer_up(): void {
|
|
926
|
+
if (this.current_subtool !== "crop") return;
|
|
927
|
+
|
|
928
|
+
this.is_dragging = false;
|
|
929
|
+
this.is_dragging_window = false;
|
|
930
|
+
this.selected_handle = null;
|
|
931
|
+
this.last_pointer_position = null;
|
|
932
|
+
this.drag_start_position = null;
|
|
933
|
+
this.drag_start_bounds = null;
|
|
934
|
+
this.active_corner_index = -1;
|
|
935
|
+
this.active_edge_index = -1;
|
|
936
|
+
|
|
937
|
+
this.notify("change");
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* handles the start of window dragging
|
|
942
|
+
* @param event - the pointer event
|
|
943
|
+
*/
|
|
944
|
+
private handle_window_drag_start(event: FederatedPointerEvent): void {
|
|
945
|
+
if (this.current_subtool !== "crop") return;
|
|
946
|
+
event.stopPropagation();
|
|
947
|
+
|
|
948
|
+
this.is_dragging_window = true;
|
|
949
|
+
// Set flag indicating the crop has been manually changed
|
|
950
|
+
this.has_been_manually_changed = true;
|
|
951
|
+
|
|
952
|
+
// Store the initial position and bounds
|
|
953
|
+
const local_pos = this.image_editor_context.image_container.toLocal(
|
|
954
|
+
event.global
|
|
955
|
+
);
|
|
956
|
+
this.drag_start_position = new Point(local_pos.x, local_pos.y);
|
|
957
|
+
this.drag_start_bounds = { ...this.crop_bounds };
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* @returns whether the crop has been manually changed by the user
|
|
962
|
+
*/
|
|
963
|
+
public get crop_manually_changed(): boolean {
|
|
964
|
+
return this.has_been_manually_changed;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
on<T extends string>(event: T, callback: () => void): void {
|
|
968
|
+
this.event_callbacks.set(event, [
|
|
969
|
+
...(this.event_callbacks.get(event) || []),
|
|
970
|
+
callback
|
|
971
|
+
]);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
off<T extends string>(event: T, callback: () => void): void {
|
|
975
|
+
this.event_callbacks.set(
|
|
976
|
+
event,
|
|
977
|
+
this.event_callbacks.get(event)?.filter((cb) => cb !== callback) || []
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
private notify<T extends string>(event: T): void {
|
|
982
|
+
for (const callback of this.event_callbacks.get(event) || []) {
|
|
983
|
+
callback();
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
public get_crop_bounds(): {
|
|
988
|
+
x: number;
|
|
989
|
+
y: number;
|
|
990
|
+
crop_dimensions: { width: number; height: number };
|
|
991
|
+
image_dimensions: { width: number; height: number };
|
|
992
|
+
} {
|
|
993
|
+
const { width, height } =
|
|
994
|
+
this.image_editor_context.background_image?.getLocalBounds() ?? {
|
|
995
|
+
width: 0,
|
|
996
|
+
height: 0
|
|
997
|
+
};
|
|
998
|
+
return {
|
|
999
|
+
x: this.crop_bounds.x,
|
|
1000
|
+
y: this.crop_bounds.y,
|
|
1001
|
+
crop_dimensions: {
|
|
1002
|
+
width: this.crop_bounds.width,
|
|
1003
|
+
height: this.crop_bounds.height
|
|
1004
|
+
},
|
|
1005
|
+
image_dimensions: {
|
|
1006
|
+
width,
|
|
1007
|
+
height
|
|
1008
|
+
}
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
public get_image(): Promise<Blob | null> {
|
|
1013
|
+
if (!this.image_editor_context.background_image)
|
|
1014
|
+
return Promise.resolve(null);
|
|
1015
|
+
const container = new Container();
|
|
1016
|
+
const sprite = new Sprite(
|
|
1017
|
+
this.image_editor_context.background_image.texture
|
|
1018
|
+
);
|
|
1019
|
+
container.addChild(sprite);
|
|
1020
|
+
|
|
1021
|
+
return get_canvas_blob(
|
|
1022
|
+
this.image_editor_context.app.renderer,
|
|
1023
|
+
container,
|
|
1024
|
+
this.crop_bounds
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
}
|