@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,729 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Point,
|
|
3
|
+
type FederatedWheelEvent,
|
|
4
|
+
type FederatedPointerEvent
|
|
5
|
+
} from "pixi.js";
|
|
6
|
+
import { type ImageEditorContext, type Tool } from "../core/editor";
|
|
7
|
+
import { type Tool as ToolbarTool, type Subtool } from "../Toolbar.svelte";
|
|
8
|
+
import { get, writable } from "svelte/store";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* ZoomTool class for handling zoom and pan functionality in the image editor
|
|
12
|
+
* Implements the Tool interface
|
|
13
|
+
*/
|
|
14
|
+
export class ZoomTool implements Tool {
|
|
15
|
+
name = "zoom";
|
|
16
|
+
min_zoom = writable(true);
|
|
17
|
+
|
|
18
|
+
private image_editor_context!: ImageEditorContext;
|
|
19
|
+
|
|
20
|
+
// Constants
|
|
21
|
+
private readonly max_zoom = 10;
|
|
22
|
+
private readonly border_padding = 30;
|
|
23
|
+
|
|
24
|
+
// State
|
|
25
|
+
private pad_bottom = 0;
|
|
26
|
+
private is_pinching = false;
|
|
27
|
+
private is_dragging = false;
|
|
28
|
+
private is_pointer_dragging = false;
|
|
29
|
+
private last_touch_position: Point | null = null;
|
|
30
|
+
private last_pinch_distance = 0;
|
|
31
|
+
private drag_start = new Point();
|
|
32
|
+
private current_tool!: ToolbarTool;
|
|
33
|
+
private current_subtool!: Subtool;
|
|
34
|
+
private local_scale = 1;
|
|
35
|
+
private local_dimensions: { width: number; height: number } = {
|
|
36
|
+
width: 0,
|
|
37
|
+
height: 0
|
|
38
|
+
};
|
|
39
|
+
private local_position: { x: number; y: number } = { x: 0, y: 0 };
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Prevents default behavior for events
|
|
43
|
+
* @param {FederatedWheelEvent | FederatedPointerEvent} event - The event to prevent default behavior for
|
|
44
|
+
*/
|
|
45
|
+
prevent_default(
|
|
46
|
+
event: FederatedWheelEvent | FederatedPointerEvent | WheelEvent
|
|
47
|
+
): void {
|
|
48
|
+
event.preventDefault();
|
|
49
|
+
event.stopPropagation();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Sets the current tool and subtool
|
|
54
|
+
* @param {ToolbarTool} tool - The tool to set
|
|
55
|
+
* @param {Subtool} subtool - The subtool to set
|
|
56
|
+
*/
|
|
57
|
+
set_tool(tool: ToolbarTool, subtool: Subtool): void {
|
|
58
|
+
this.current_tool = tool;
|
|
59
|
+
this.current_subtool = subtool;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Sets the zoom level to a specific percentage or fits the image to the screen
|
|
64
|
+
* @param {number | 'fit'} zoom_level - The zoom level to set (0-1) or 'fit' to use min_zoom
|
|
65
|
+
* @returns {void}
|
|
66
|
+
*/
|
|
67
|
+
set_zoom(zoom_level: number | "fit"): void {
|
|
68
|
+
// Calculate the min zoom level
|
|
69
|
+
const fit_zoom = this.calculate_min_zoom(
|
|
70
|
+
this.local_dimensions.width,
|
|
71
|
+
this.local_dimensions.height
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// Determine the target zoom level
|
|
75
|
+
let target_zoom: number;
|
|
76
|
+
const is_fit_zoom = zoom_level === "fit";
|
|
77
|
+
if (is_fit_zoom) {
|
|
78
|
+
target_zoom = fit_zoom;
|
|
79
|
+
} else {
|
|
80
|
+
// Ensure zoom_level is between 0 and 1, then scale it to the range [min_zoom, max_zoom]
|
|
81
|
+
target_zoom = Math.max(0, Math.min(this.max_zoom, zoom_level));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Get the canvas dimensions
|
|
85
|
+
const canvas = this.image_editor_context.app.screen;
|
|
86
|
+
const canvas_width = canvas.width;
|
|
87
|
+
const canvas_height = canvas.height;
|
|
88
|
+
|
|
89
|
+
// Determine the center point for zooming
|
|
90
|
+
let center_point: { x: number; y: number };
|
|
91
|
+
|
|
92
|
+
// Always use the center of the screen
|
|
93
|
+
center_point = {
|
|
94
|
+
x: canvas_width / 2,
|
|
95
|
+
y: canvas_height / 2
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Apply the zoom using the existing zoom_to_point method
|
|
99
|
+
this.zoom_to_point(target_zoom, center_point, true, is_fit_zoom);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Sets up the zoom tool with the given context and state
|
|
104
|
+
* @param {ImageEditorContext} context - The image editor context
|
|
105
|
+
* @param {ToolbarTool} tool - The current tool
|
|
106
|
+
* @param {Subtool} subtool - The current subtool
|
|
107
|
+
* @returns {Promise<void>}
|
|
108
|
+
*/
|
|
109
|
+
async setup(
|
|
110
|
+
context: ImageEditorContext,
|
|
111
|
+
tool: ToolbarTool,
|
|
112
|
+
subtool: Subtool
|
|
113
|
+
): Promise<void> {
|
|
114
|
+
this.image_editor_context = context;
|
|
115
|
+
this.current_tool = tool;
|
|
116
|
+
this.current_subtool = subtool;
|
|
117
|
+
this.pad_bottom = context.pad_bottom;
|
|
118
|
+
// Initialize zoom and position
|
|
119
|
+
const { width, height } = await this.get_container_dimensions();
|
|
120
|
+
const fit_zoom = this.calculate_min_zoom(width, height);
|
|
121
|
+
// Use fit zoom or 1, whichever is smaller
|
|
122
|
+
const min_zoom = Math.min(fit_zoom, 1);
|
|
123
|
+
|
|
124
|
+
// Set initial zoom
|
|
125
|
+
this.local_scale = min_zoom;
|
|
126
|
+
|
|
127
|
+
// Use simple center positioning
|
|
128
|
+
const canvas = this.image_editor_context.app.screen;
|
|
129
|
+
|
|
130
|
+
const center_x = canvas.width / 2;
|
|
131
|
+
const center_y = canvas.height / 2;
|
|
132
|
+
|
|
133
|
+
// Calculate initial position to center the scaled image
|
|
134
|
+
const scaled_width = width * min_zoom;
|
|
135
|
+
const scaled_height = height * min_zoom;
|
|
136
|
+
const initial_x = center_x - scaled_width / 2;
|
|
137
|
+
const initial_y = center_y - scaled_height / 2;
|
|
138
|
+
|
|
139
|
+
this.local_position = { x: initial_x, y: initial_y };
|
|
140
|
+
|
|
141
|
+
// Setup event listeners
|
|
142
|
+
this.setup_event_listeners();
|
|
143
|
+
|
|
144
|
+
// Update context with initial values
|
|
145
|
+
this.image_editor_context.set_image_properties({
|
|
146
|
+
scale: min_zoom,
|
|
147
|
+
position: { x: initial_x, y: initial_y }
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
this.image_editor_context.dimensions.subscribe((dimensions) => {
|
|
151
|
+
this.local_dimensions = dimensions;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
this.image_editor_context.scale.subscribe((scale) => {
|
|
155
|
+
this.local_scale = scale;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
this.image_editor_context.position.subscribe((position) => {
|
|
159
|
+
this.local_position = position;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Sets up event listeners for zoom and pan functionality
|
|
165
|
+
* @private
|
|
166
|
+
*/
|
|
167
|
+
private setup_event_listeners(): void {
|
|
168
|
+
const stage = this.image_editor_context.app.stage;
|
|
169
|
+
const canvas = this.image_editor_context.app.canvas;
|
|
170
|
+
|
|
171
|
+
canvas.addEventListener("wheel", this.prevent_default, { passive: false });
|
|
172
|
+
|
|
173
|
+
// Configure stage for interaction
|
|
174
|
+
stage.eventMode = "static";
|
|
175
|
+
stage.hitArea = this.image_editor_context.app.screen;
|
|
176
|
+
|
|
177
|
+
// Add wheel event with options
|
|
178
|
+
const wheel_listener = this.handle_wheel.bind(this);
|
|
179
|
+
stage.addEventListener("wheel", wheel_listener, { passive: false });
|
|
180
|
+
|
|
181
|
+
// Setup touch/pointer events based on device
|
|
182
|
+
if ("ontouchstart" in window) {
|
|
183
|
+
stage.addEventListener(
|
|
184
|
+
"touchstart",
|
|
185
|
+
this.handle_touch_start.bind(this) as EventListener
|
|
186
|
+
);
|
|
187
|
+
stage.addEventListener(
|
|
188
|
+
"touchmove",
|
|
189
|
+
this.handle_touch_move.bind(this) as EventListener
|
|
190
|
+
);
|
|
191
|
+
stage.addEventListener(
|
|
192
|
+
"touchend",
|
|
193
|
+
this.handle_touch_end.bind(this) as EventListener
|
|
194
|
+
);
|
|
195
|
+
} else {
|
|
196
|
+
stage.addEventListener(
|
|
197
|
+
"pointerdown",
|
|
198
|
+
this.handle_pointer_down.bind(this)
|
|
199
|
+
);
|
|
200
|
+
stage.addEventListener(
|
|
201
|
+
"pointermove",
|
|
202
|
+
this.handle_pointer_move.bind(this)
|
|
203
|
+
);
|
|
204
|
+
stage.addEventListener("pointerup", this.handle_pointer_up.bind(this));
|
|
205
|
+
stage.addEventListener(
|
|
206
|
+
"pointerupoutside",
|
|
207
|
+
this.handle_pointer_up.bind(this)
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Handles wheel events for zooming
|
|
214
|
+
* @param {FederatedWheelEvent} event - The wheel event
|
|
215
|
+
* @private
|
|
216
|
+
*/
|
|
217
|
+
private handle_wheel(event: FederatedWheelEvent): void {
|
|
218
|
+
const is_trackpad = event.deltaMode === 0 && Math.abs(event.deltaY) < 50;
|
|
219
|
+
const scroll_speed = is_trackpad ? 30 : 10;
|
|
220
|
+
|
|
221
|
+
if (event.altKey || event.metaKey) {
|
|
222
|
+
// Handle zooming with our new simplified logic
|
|
223
|
+
const local_point = this.image_editor_context.app.stage.toLocal(
|
|
224
|
+
event.global
|
|
225
|
+
);
|
|
226
|
+
const zoom_delta = -event.deltaY * (is_trackpad ? 0.001 : 0.0005);
|
|
227
|
+
const new_scale = this.local_scale * (1 + zoom_delta);
|
|
228
|
+
this.zoom_to_point(new_scale, local_point, true);
|
|
229
|
+
} else {
|
|
230
|
+
// Handle panning with bounds
|
|
231
|
+
const delta_x = event.deltaX;
|
|
232
|
+
const delta_y = event.deltaY;
|
|
233
|
+
const raw_position = {
|
|
234
|
+
x: this.local_position.x - (delta_x / 100) * scroll_speed,
|
|
235
|
+
y: this.local_position.y - (delta_y / 100) * scroll_speed
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const canvas = this.image_editor_context.app.screen;
|
|
239
|
+
const scaled_width = this.local_dimensions.width * this.local_scale;
|
|
240
|
+
const scaled_height = this.local_dimensions.height * this.local_scale;
|
|
241
|
+
const available_width = canvas.width - this.border_padding * 2;
|
|
242
|
+
const available_height =
|
|
243
|
+
canvas.height - this.border_padding * 2 - this.pad_bottom;
|
|
244
|
+
|
|
245
|
+
let final_position = { ...raw_position };
|
|
246
|
+
|
|
247
|
+
// If image is smaller than available space, center it
|
|
248
|
+
if (scaled_width <= available_width) {
|
|
249
|
+
final_position.x = (canvas.width - scaled_width) / 2;
|
|
250
|
+
} else {
|
|
251
|
+
// Apply 50% rule for horizontal bounds
|
|
252
|
+
const max_offset = canvas.width / 2;
|
|
253
|
+
const left_bound = max_offset;
|
|
254
|
+
const right_bound = canvas.width - max_offset - scaled_width;
|
|
255
|
+
final_position.x = Math.min(
|
|
256
|
+
Math.max(raw_position.x, right_bound),
|
|
257
|
+
left_bound
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (scaled_height <= available_height) {
|
|
262
|
+
final_position.y =
|
|
263
|
+
(canvas.height - this.pad_bottom - scaled_height) / 2;
|
|
264
|
+
} else {
|
|
265
|
+
// Apply 50% rule for vertical bounds, accounting for pad_bottom
|
|
266
|
+
const max_offset = (canvas.height - this.pad_bottom) / 2;
|
|
267
|
+
const top_bound = max_offset;
|
|
268
|
+
const bottom_bound =
|
|
269
|
+
canvas.height - this.pad_bottom - max_offset - scaled_height;
|
|
270
|
+
final_position.y = Math.min(
|
|
271
|
+
Math.max(raw_position.y, bottom_bound),
|
|
272
|
+
top_bound
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
this.image_editor_context.set_image_properties({
|
|
277
|
+
scale: this.local_scale,
|
|
278
|
+
position: final_position
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Cleans up resources and event listeners
|
|
285
|
+
*/
|
|
286
|
+
cleanup(): void {
|
|
287
|
+
const stage = this?.image_editor_context?.app?.stage;
|
|
288
|
+
if (!stage) return;
|
|
289
|
+
stage.removeEventListener("wheel", this.handle_wheel.bind(this));
|
|
290
|
+
|
|
291
|
+
if ("ontouchstart" in window) {
|
|
292
|
+
stage.removeEventListener(
|
|
293
|
+
"touchstart",
|
|
294
|
+
this.handle_touch_start.bind(this) as EventListener
|
|
295
|
+
);
|
|
296
|
+
stage.removeEventListener(
|
|
297
|
+
"touchmove",
|
|
298
|
+
this.handle_touch_move.bind(this) as EventListener
|
|
299
|
+
);
|
|
300
|
+
stage.removeEventListener(
|
|
301
|
+
"touchend",
|
|
302
|
+
this.handle_touch_end.bind(this) as EventListener
|
|
303
|
+
);
|
|
304
|
+
} else {
|
|
305
|
+
stage.removeEventListener(
|
|
306
|
+
"pointerdown",
|
|
307
|
+
this.handle_pointer_down.bind(this)
|
|
308
|
+
);
|
|
309
|
+
stage.removeEventListener(
|
|
310
|
+
"pointermove",
|
|
311
|
+
this.handle_pointer_move.bind(this)
|
|
312
|
+
);
|
|
313
|
+
stage.removeEventListener("pointerup", this.handle_pointer_up.bind(this));
|
|
314
|
+
stage.removeEventListener(
|
|
315
|
+
"pointerupoutside",
|
|
316
|
+
this.handle_pointer_up.bind(this)
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Gets the dimensions of the container
|
|
323
|
+
* @returns {Promise<{width: number, height: number}>} The container dimensions
|
|
324
|
+
* @private
|
|
325
|
+
*/
|
|
326
|
+
private async get_container_dimensions(): Promise<{
|
|
327
|
+
width: number;
|
|
328
|
+
height: number;
|
|
329
|
+
}> {
|
|
330
|
+
const bounds = this.image_editor_context.image_container.getLocalBounds();
|
|
331
|
+
return {
|
|
332
|
+
width: bounds.width,
|
|
333
|
+
height: bounds.height
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Calculates the minimum zoom level to fit the image in the viewport
|
|
339
|
+
* @param {number} container_width - The width of the container
|
|
340
|
+
* @param {number} container_height - The height of the container
|
|
341
|
+
* @returns {number} The minimum zoom level
|
|
342
|
+
* @private
|
|
343
|
+
*/
|
|
344
|
+
private calculate_min_zoom(
|
|
345
|
+
container_width: number,
|
|
346
|
+
container_height: number
|
|
347
|
+
): number {
|
|
348
|
+
const canvas = this.image_editor_context.app.screen;
|
|
349
|
+
const viewport_width = canvas.width;
|
|
350
|
+
const viewport_height = canvas.height;
|
|
351
|
+
|
|
352
|
+
if (
|
|
353
|
+
!container_width ||
|
|
354
|
+
!viewport_width ||
|
|
355
|
+
!container_height ||
|
|
356
|
+
!viewport_height
|
|
357
|
+
) {
|
|
358
|
+
return 1;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Calculate available space accounting for padding
|
|
362
|
+
const available_width = viewport_width - this.border_padding * 2;
|
|
363
|
+
const available_height =
|
|
364
|
+
viewport_height - this.border_padding * 2 - this.pad_bottom;
|
|
365
|
+
|
|
366
|
+
// Calculate zoom ratios for both dimensions
|
|
367
|
+
const width_ratio = available_width / container_width;
|
|
368
|
+
const height_ratio = available_height / container_height;
|
|
369
|
+
|
|
370
|
+
// Use the smaller ratio to ensure the image fits in both dimensions
|
|
371
|
+
// Note: we no longer cap at 1, as 'fit' can be above 1
|
|
372
|
+
return Math.min(width_ratio, height_ratio);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Handles touch start events
|
|
377
|
+
* @param {TouchEvent} event - The touch event
|
|
378
|
+
* @private
|
|
379
|
+
*/
|
|
380
|
+
private handle_touch_start(event: FederatedPointerEvent): void {
|
|
381
|
+
event.preventDefault();
|
|
382
|
+
|
|
383
|
+
// Cast to any to access TouchEvent properties
|
|
384
|
+
const touchEvent = event as any;
|
|
385
|
+
|
|
386
|
+
if (touchEvent.touches && touchEvent.touches.length === 2) {
|
|
387
|
+
this.is_pinching = true;
|
|
388
|
+
this.last_pinch_distance = Math.hypot(
|
|
389
|
+
touchEvent.touches[0].pageX - touchEvent.touches[1].pageX,
|
|
390
|
+
touchEvent.touches[0].pageY - touchEvent.touches[1].pageY
|
|
391
|
+
);
|
|
392
|
+
} else if (touchEvent.touches && touchEvent.touches.length === 1) {
|
|
393
|
+
this.is_dragging = true;
|
|
394
|
+
const rect = this.image_editor_context.app.view.getBoundingClientRect();
|
|
395
|
+
this.last_touch_position = new Point(
|
|
396
|
+
touchEvent.touches[0].pageX - rect.left,
|
|
397
|
+
touchEvent.touches[0].pageY - rect.top
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Handles touch move events
|
|
404
|
+
* @param {TouchEvent} event - The touch event
|
|
405
|
+
* @private
|
|
406
|
+
*/
|
|
407
|
+
private handle_touch_move(event: FederatedPointerEvent): void {
|
|
408
|
+
event.preventDefault();
|
|
409
|
+
|
|
410
|
+
// Cast to any to access TouchEvent properties
|
|
411
|
+
const touchEvent = event as any;
|
|
412
|
+
|
|
413
|
+
if (
|
|
414
|
+
this.is_pinching &&
|
|
415
|
+
touchEvent.touches &&
|
|
416
|
+
touchEvent.touches.length === 2
|
|
417
|
+
) {
|
|
418
|
+
const rect = this.image_editor_context.app.view.getBoundingClientRect();
|
|
419
|
+
const current_distance = Math.hypot(
|
|
420
|
+
touchEvent.touches[0].pageX - touchEvent.touches[1].pageX,
|
|
421
|
+
touchEvent.touches[0].pageY - touchEvent.touches[1].pageY
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
const pinch_center = {
|
|
425
|
+
x:
|
|
426
|
+
(touchEvent.touches[0].pageX + touchEvent.touches[1].pageX) / 2 -
|
|
427
|
+
rect.left,
|
|
428
|
+
y:
|
|
429
|
+
(touchEvent.touches[0].pageY + touchEvent.touches[1].pageY) / 2 -
|
|
430
|
+
rect.top
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const scale_val = current_distance / this.last_pinch_distance;
|
|
434
|
+
this.last_pinch_distance = current_distance;
|
|
435
|
+
|
|
436
|
+
// Update zoom based on pinch
|
|
437
|
+
this.zoom_to_point(this.local_scale * scale_val, pinch_center);
|
|
438
|
+
} else if (
|
|
439
|
+
this.is_dragging &&
|
|
440
|
+
touchEvent.touches &&
|
|
441
|
+
touchEvent.touches.length === 1 &&
|
|
442
|
+
this.last_touch_position
|
|
443
|
+
) {
|
|
444
|
+
const rect = this.image_editor_context.app.view.getBoundingClientRect();
|
|
445
|
+
const current_position = new Point(
|
|
446
|
+
touchEvent.touches[0].pageX - rect.left,
|
|
447
|
+
touchEvent.touches[0].pageY - rect.top
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
const dx = current_position.x - this.last_touch_position.x;
|
|
451
|
+
const dy = current_position.y - this.last_touch_position.y;
|
|
452
|
+
|
|
453
|
+
// Update position based on drag
|
|
454
|
+
this.image_editor_context.set_image_properties({
|
|
455
|
+
position: {
|
|
456
|
+
x: this.local_position.x + dx,
|
|
457
|
+
y: this.local_position.y + dy
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
this.last_touch_position = current_position;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Handles touch end events
|
|
467
|
+
* @param {TouchEvent} event - The touch event
|
|
468
|
+
* @private
|
|
469
|
+
*/
|
|
470
|
+
private handle_touch_end(event: FederatedPointerEvent): void {
|
|
471
|
+
event.preventDefault();
|
|
472
|
+
|
|
473
|
+
// Cast to any to access TouchEvent properties
|
|
474
|
+
const touchEvent = event as any;
|
|
475
|
+
|
|
476
|
+
if (touchEvent.touches && touchEvent.touches.length < 2) {
|
|
477
|
+
this.is_pinching = false;
|
|
478
|
+
if (touchEvent.touches && touchEvent.touches.length === 1) {
|
|
479
|
+
const rect = this.image_editor_context.app.view.getBoundingClientRect();
|
|
480
|
+
this.last_touch_position = new Point(
|
|
481
|
+
touchEvent.touches[0].pageX - rect.left,
|
|
482
|
+
touchEvent.touches[0].pageY - rect.top
|
|
483
|
+
);
|
|
484
|
+
this.is_dragging = true;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (touchEvent.touches && touchEvent.touches.length === 0) {
|
|
488
|
+
this.is_dragging = false;
|
|
489
|
+
this.last_touch_position = null;
|
|
490
|
+
this.last_pinch_distance = 0;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Gets the bounded position to keep the image within the viewport
|
|
496
|
+
* @param {Object} position - The position to bound
|
|
497
|
+
* @param {number} position.x - The x coordinate
|
|
498
|
+
* @param {number} position.y - The y coordinate
|
|
499
|
+
* @returns {Object} The bounded position
|
|
500
|
+
* @private
|
|
501
|
+
*/
|
|
502
|
+
private get_bounded_position(position: { x: number; y: number }): {
|
|
503
|
+
x: number;
|
|
504
|
+
y: number;
|
|
505
|
+
} {
|
|
506
|
+
const canvas = this.image_editor_context.app.screen;
|
|
507
|
+
const scaled_width = this.local_dimensions.width * this.local_scale;
|
|
508
|
+
const scaled_height = this.local_dimensions.height * this.local_scale;
|
|
509
|
+
|
|
510
|
+
// Calculate center position, accounting for pad_bottom
|
|
511
|
+
const center_position = {
|
|
512
|
+
x: (canvas.width - scaled_width) / 2,
|
|
513
|
+
y: (canvas.height - scaled_height - this.pad_bottom) / 2
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
if (scaled_width <= canvas.width && scaled_height <= canvas.height) {
|
|
517
|
+
return center_position;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
let x = position.x;
|
|
521
|
+
let y = position.y;
|
|
522
|
+
|
|
523
|
+
if (scaled_width <= canvas.width) {
|
|
524
|
+
x = center_position.x;
|
|
525
|
+
} else {
|
|
526
|
+
const min_x = canvas.width - scaled_width;
|
|
527
|
+
const max_x = 0;
|
|
528
|
+
x = Math.max(min_x, Math.min(max_x, position.x));
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (scaled_height <= canvas.height - this.pad_bottom) {
|
|
532
|
+
y = center_position.y;
|
|
533
|
+
} else {
|
|
534
|
+
const min_y = canvas.height - scaled_height - this.pad_bottom;
|
|
535
|
+
const max_y = 0;
|
|
536
|
+
y = Math.max(min_y, Math.min(max_y, position.y));
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return { x, y };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Zooms to a specific point with a new zoom level
|
|
544
|
+
* @param {number} new_zoom - The new zoom level
|
|
545
|
+
* @param {Object} point - The point to zoom to
|
|
546
|
+
* @param {number} point.x - The x coordinate of the point
|
|
547
|
+
* @param {number} point.y - The y coordinate of the point
|
|
548
|
+
* @param {boolean} hard - Whether to apply a hard zoom (no animation)
|
|
549
|
+
* @param {boolean} is_fit_zoom - Whether this is a fit zoom operation
|
|
550
|
+
* @private
|
|
551
|
+
*/
|
|
552
|
+
private zoom_to_point(
|
|
553
|
+
new_zoom: number,
|
|
554
|
+
point: { x: number; y: number },
|
|
555
|
+
hard?: boolean,
|
|
556
|
+
is_fit_zoom?: boolean
|
|
557
|
+
): void {
|
|
558
|
+
// Get the cursor position relative to the image's top-left corner
|
|
559
|
+
const cursor_relative_to_image = {
|
|
560
|
+
x: (point.x - this.local_position.x) / this.local_scale,
|
|
561
|
+
y: (point.y - this.local_position.y) / this.local_scale
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
// Calculate what percentage of the image width/height this represents
|
|
565
|
+
const image_percentages = {
|
|
566
|
+
x: cursor_relative_to_image.x / this.local_dimensions.width,
|
|
567
|
+
y: cursor_relative_to_image.y / this.local_dimensions.height
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
// Apply zoom limits
|
|
571
|
+
const fit_zoom = this.calculate_min_zoom(
|
|
572
|
+
this.local_dimensions.width,
|
|
573
|
+
this.local_dimensions.height
|
|
574
|
+
);
|
|
575
|
+
// Use the fit zoom or 1, whichever is smaller as the minimum zoom
|
|
576
|
+
const min_zoom = Math.min(fit_zoom, 1);
|
|
577
|
+
new_zoom = Math.min(Math.max(new_zoom, min_zoom), this.max_zoom);
|
|
578
|
+
|
|
579
|
+
// Calculate new dimensions
|
|
580
|
+
const new_scaled_width = this.local_dimensions.width * new_zoom;
|
|
581
|
+
const new_scaled_height = this.local_dimensions.height * new_zoom;
|
|
582
|
+
|
|
583
|
+
// Calculate new position maintaining the same relative cursor position
|
|
584
|
+
let new_position = {
|
|
585
|
+
x: point.x - new_scaled_width * image_percentages.x,
|
|
586
|
+
y: point.y - new_scaled_height * image_percentages.y
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
// Center the image if we're at minimum zoom or if this is a fit zoom operation
|
|
590
|
+
if (new_zoom === min_zoom || is_fit_zoom) {
|
|
591
|
+
const canvas_width = this.image_editor_context.app.screen.width;
|
|
592
|
+
const canvas_height = this.image_editor_context.app.screen.height;
|
|
593
|
+
|
|
594
|
+
// Center the image, accounting for pad_bottom
|
|
595
|
+
new_position = {
|
|
596
|
+
x: (canvas_width - new_scaled_width) / 2,
|
|
597
|
+
y: (canvas_height - this.pad_bottom - new_scaled_height) / 2
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
this.image_editor_context.set_image_properties({
|
|
602
|
+
scale: new_zoom,
|
|
603
|
+
position: new_position,
|
|
604
|
+
animate: typeof hard === "boolean" ? !hard : new_zoom === min_zoom
|
|
605
|
+
});
|
|
606
|
+
this.min_zoom.set(new_zoom === min_zoom);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Handles pointer down events
|
|
611
|
+
* @param {FederatedPointerEvent} event - The pointer event
|
|
612
|
+
* @private
|
|
613
|
+
*/
|
|
614
|
+
private handle_pointer_down(event: FederatedPointerEvent): void {
|
|
615
|
+
// Only allow dragging if the current tool is 'pan'
|
|
616
|
+
if (event.button === 0 && this.current_tool === "pan") {
|
|
617
|
+
this.is_pointer_dragging = true;
|
|
618
|
+
this.drag_start.copyFrom(event.global);
|
|
619
|
+
this.drag_start.x -= this.local_position.x;
|
|
620
|
+
this.drag_start.y -= this.local_position.y;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Handles pointer move events
|
|
626
|
+
* @param {FederatedPointerEvent} event - The pointer event
|
|
627
|
+
* @private
|
|
628
|
+
*/
|
|
629
|
+
private handle_pointer_move(event: FederatedPointerEvent): void {
|
|
630
|
+
// Only handle move if we're dragging and the tool is 'pan'
|
|
631
|
+
if (this.is_pointer_dragging && this.current_tool === "pan") {
|
|
632
|
+
const raw_position = {
|
|
633
|
+
x: event.global.x - this.drag_start.x,
|
|
634
|
+
y: event.global.y - this.drag_start.y
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
const canvas = this.image_editor_context.app.screen;
|
|
638
|
+
const scaled_width = this.local_dimensions.width * this.local_scale;
|
|
639
|
+
const scaled_height = this.local_dimensions.height * this.local_scale;
|
|
640
|
+
const available_width = canvas.width - this.border_padding * 2;
|
|
641
|
+
const available_height =
|
|
642
|
+
canvas.height - this.border_padding * 2 - this.pad_bottom;
|
|
643
|
+
|
|
644
|
+
let final_position = { ...raw_position };
|
|
645
|
+
|
|
646
|
+
// If image is smaller than available space, center it
|
|
647
|
+
if (scaled_width <= available_width) {
|
|
648
|
+
final_position.x = (canvas.width - scaled_width) / 2;
|
|
649
|
+
} else {
|
|
650
|
+
// Allow free movement during drag, bounds will be applied on pointer up
|
|
651
|
+
final_position.x = raw_position.x;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (scaled_height <= available_height) {
|
|
655
|
+
final_position.y =
|
|
656
|
+
(canvas.height - this.pad_bottom - scaled_height) / 2;
|
|
657
|
+
} else {
|
|
658
|
+
// Allow free movement during drag, bounds will be applied on pointer up
|
|
659
|
+
final_position.y = raw_position.y;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
this.image_editor_context.set_image_properties({
|
|
663
|
+
scale: this.local_scale,
|
|
664
|
+
position: final_position
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Handles pointer up events
|
|
671
|
+
* @param {FederatedPointerEvent} event - The pointer event
|
|
672
|
+
* @private
|
|
673
|
+
*/
|
|
674
|
+
private handle_pointer_up(event: FederatedPointerEvent): void {
|
|
675
|
+
// Only handle up if we're dragging and the tool is 'pan'
|
|
676
|
+
if (this.is_pointer_dragging && this.current_tool === "pan") {
|
|
677
|
+
this.is_pointer_dragging = false;
|
|
678
|
+
|
|
679
|
+
const raw_position = {
|
|
680
|
+
x: event.global.x - this.drag_start.x,
|
|
681
|
+
y: event.global.y - this.drag_start.y
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
const canvas = this.image_editor_context.app.screen;
|
|
685
|
+
const scaled_width = this.local_dimensions.width * this.local_scale;
|
|
686
|
+
const scaled_height = this.local_dimensions.height * this.local_scale;
|
|
687
|
+
const available_width = canvas.width - this.border_padding * 2;
|
|
688
|
+
const available_height =
|
|
689
|
+
canvas.height - this.border_padding * 2 - this.pad_bottom;
|
|
690
|
+
|
|
691
|
+
let final_position = { ...raw_position };
|
|
692
|
+
|
|
693
|
+
// If image is smaller than available space, center it
|
|
694
|
+
if (scaled_width <= available_width) {
|
|
695
|
+
final_position.x = (canvas.width - scaled_width) / 2;
|
|
696
|
+
} else {
|
|
697
|
+
// Apply 50% rule for horizontal snapping
|
|
698
|
+
const max_offset = canvas.width / 2;
|
|
699
|
+
const left_bound = max_offset;
|
|
700
|
+
const right_bound = canvas.width - max_offset - scaled_width;
|
|
701
|
+
final_position.x = Math.min(
|
|
702
|
+
Math.max(raw_position.x, right_bound),
|
|
703
|
+
left_bound
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (scaled_height <= available_height) {
|
|
708
|
+
final_position.y =
|
|
709
|
+
(canvas.height - this.pad_bottom - scaled_height) / 2;
|
|
710
|
+
} else {
|
|
711
|
+
// Apply 50% rule for vertical snapping, accounting for pad_bottom
|
|
712
|
+
const max_offset = (canvas.height - this.pad_bottom) / 2;
|
|
713
|
+
const top_bound = max_offset;
|
|
714
|
+
const bottom_bound =
|
|
715
|
+
canvas.height - this.pad_bottom - max_offset - scaled_height;
|
|
716
|
+
final_position.y = Math.min(
|
|
717
|
+
Math.max(raw_position.y, bottom_bound),
|
|
718
|
+
top_bound
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
this.image_editor_context.set_image_properties({
|
|
723
|
+
scale: this.local_scale,
|
|
724
|
+
position: final_position,
|
|
725
|
+
animate: true
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|