@h4md1/visual-image-tool 0.2.2 → 0.2.4
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/README.md +21 -5
- package/dist/{visual-image-tool.js → visual-image-tool.cjs} +251 -159
- package/dist/visual-image-tool.cjs.map +1 -0
- package/dist/visual-image-tool.esm.js +251 -156
- package/dist/visual-image-tool.esm.js.map +1 -1
- package/dist/visual-image-tool.umd.js +1 -1
- package/dist/visual-image-tool.umd.js.map +1 -1
- package/package.json +26 -10
- package/src/index.js +3 -6
- package/src/visual-image-tool.js +251 -151
- package/src/visual-image-tool.test.js +150 -0
- package/dist/visual-image-tool.js.map +0 -1
|
@@ -1,42 +1,41 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
-
|
|
5
3
|
/**
|
|
6
|
-
* ImageTool -
|
|
4
|
+
* ImageTool - A lightweight tool for defining focus points and crop zones on images
|
|
7
5
|
* @module visual-image-tool
|
|
8
6
|
*/
|
|
9
7
|
|
|
10
8
|
class VisualImageTool {
|
|
11
9
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* @param {
|
|
15
|
-
* @param {
|
|
16
|
-
* @param {
|
|
17
|
-
* @param {
|
|
18
|
-
* @param {Object} [options.
|
|
19
|
-
* @param {
|
|
20
|
-
* @param {
|
|
21
|
-
* @param {
|
|
10
|
+
* Creates an instance of the image tool.
|
|
11
|
+
* The focus point is initialized to `{x:0, y:0}` in `this.state`. When `toggleFocusPoint(true)` is called for the first time, if the focus point is still `{x:0, y:0}`, it will be automatically centered on the image.
|
|
12
|
+
* @param {Object} options - Configuration options
|
|
13
|
+
* @param {HTMLElement|string} options.imageElement - Image element or CSS selector
|
|
14
|
+
* @param {Object} [options.focusPoint] - Focus point configuration
|
|
15
|
+
* @param {boolean} [options.focusPoint.enabled=true] - Enable focus point functionality
|
|
16
|
+
* @param {Object} [options.focusPoint.style] - Custom styles for the focus point marker
|
|
17
|
+
* @param {Object} [options.cropZone] - Crop zone configuration
|
|
18
|
+
* @param {boolean} [options.cropZone.enabled=true] - Enable crop zone functionality
|
|
19
|
+
* @param {Object} [options.cropZone.style] - Custom styles for the crop overlay
|
|
20
|
+
* @param {Function} [options.onChange] - Callback called on changes
|
|
22
21
|
*/
|
|
23
22
|
constructor(options) {
|
|
24
|
-
//
|
|
23
|
+
// Validate options
|
|
25
24
|
if (!options || !options.imageElement) {
|
|
26
|
-
throw new Error("
|
|
25
|
+
throw new Error("Image element is required");
|
|
27
26
|
}
|
|
28
27
|
|
|
29
|
-
//
|
|
28
|
+
// Initialize properties
|
|
30
29
|
this.imageElement =
|
|
31
30
|
typeof options.imageElement === "string"
|
|
32
31
|
? document.querySelector(options.imageElement)
|
|
33
32
|
: options.imageElement;
|
|
34
33
|
|
|
35
34
|
if (!this.imageElement || this.imageElement.tagName !== "IMG") {
|
|
36
|
-
throw new Error("
|
|
35
|
+
throw new Error("Invalid image element");
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
//
|
|
38
|
+
// Default options
|
|
40
39
|
this.options = {
|
|
41
40
|
focusPoint: {
|
|
42
41
|
enabled: true,
|
|
@@ -68,7 +67,7 @@ class VisualImageTool {
|
|
|
68
67
|
debug: options.debug || false,
|
|
69
68
|
};
|
|
70
69
|
|
|
71
|
-
//
|
|
70
|
+
// Internal state
|
|
72
71
|
this.state = {
|
|
73
72
|
focusMarker: null,
|
|
74
73
|
cropOverlay: null,
|
|
@@ -84,7 +83,21 @@ class VisualImageTool {
|
|
|
84
83
|
scaleY: 1,
|
|
85
84
|
};
|
|
86
85
|
|
|
87
|
-
//
|
|
86
|
+
// Handlers are bound once here: addEventListener and removeEventListener
|
|
87
|
+
// must be handed the same function reference for removal to work.
|
|
88
|
+
this._boundUpdateScaling = this._updateScaling.bind(this);
|
|
89
|
+
this._boundHandleMouseUp = this._handleMouseUp.bind(this);
|
|
90
|
+
this._boundHandleMouseMove = this._handleMouseMove.bind(this);
|
|
91
|
+
this._boundFocusMarkerMouseDown =
|
|
92
|
+
this._handleFocusMarkerMouseDown.bind(this);
|
|
93
|
+
this._boundCropOverlayMouseDown =
|
|
94
|
+
this._handleCropOverlayMouseDown.bind(this);
|
|
95
|
+
this._boundCropHandleMouseDown = this._handleCropHandleMouseDown.bind(this);
|
|
96
|
+
|
|
97
|
+
// Resize handles, tracked so their listeners can be removed on destroy
|
|
98
|
+
this.cropHandles = [];
|
|
99
|
+
|
|
100
|
+
// Variables for tracking interactions
|
|
88
101
|
this.interaction = {
|
|
89
102
|
focusDragging: false,
|
|
90
103
|
focusDragOffsetX: 0,
|
|
@@ -100,22 +113,25 @@ class VisualImageTool {
|
|
|
100
113
|
startMouseY: 0,
|
|
101
114
|
};
|
|
102
115
|
|
|
103
|
-
|
|
116
|
+
this.spinnerElement = null;
|
|
117
|
+
this.initialLoadComplete = false;
|
|
118
|
+
|
|
119
|
+
// Initialize the tool
|
|
104
120
|
this._init();
|
|
105
121
|
}
|
|
106
122
|
|
|
107
123
|
/**
|
|
108
|
-
*
|
|
124
|
+
* Initializes the image tool
|
|
109
125
|
* @private
|
|
110
126
|
*/
|
|
111
127
|
_init() {
|
|
112
|
-
//
|
|
128
|
+
// Prepare the parent container
|
|
113
129
|
this._prepareContainer();
|
|
114
130
|
|
|
115
|
-
//
|
|
131
|
+
// Initialize dimensions
|
|
116
132
|
this._updateScaling();
|
|
117
133
|
|
|
118
|
-
//
|
|
134
|
+
// Create UI elements if enabled
|
|
119
135
|
if (this.options.focusPoint.enabled) {
|
|
120
136
|
this._createFocusMarker();
|
|
121
137
|
}
|
|
@@ -124,34 +140,82 @@ class VisualImageTool {
|
|
|
124
140
|
this._createCropOverlay();
|
|
125
141
|
}
|
|
126
142
|
|
|
127
|
-
//
|
|
143
|
+
// Add event listeners
|
|
128
144
|
this._setupEventListeners();
|
|
129
145
|
}
|
|
130
146
|
|
|
131
147
|
/**
|
|
132
|
-
*
|
|
148
|
+
* Prepares the image's parent container
|
|
133
149
|
* @private
|
|
134
150
|
*/
|
|
135
151
|
_prepareContainer() {
|
|
136
|
-
//
|
|
152
|
+
// Ensure the parent is positioned
|
|
137
153
|
if (this.imageElement.parentNode) {
|
|
138
154
|
this.imageElement.parentNode.style.position = "relative";
|
|
155
|
+
|
|
156
|
+
// Create spinner element
|
|
157
|
+
this.spinnerElement = document.createElement("div");
|
|
158
|
+
this.spinnerElement.style.position = "absolute";
|
|
159
|
+
this.spinnerElement.style.top = "50%";
|
|
160
|
+
this.spinnerElement.style.left = "50%";
|
|
161
|
+
this.spinnerElement.style.transform = "translate(-50%, -50%)";
|
|
162
|
+
this.spinnerElement.style.width = "40px";
|
|
163
|
+
this.spinnerElement.style.height = "40px";
|
|
164
|
+
this.spinnerElement.style.border = "4px solid #f3f3f3"; // Light grey
|
|
165
|
+
this.spinnerElement.style.borderTop = "4px solid #3498db"; // Blue
|
|
166
|
+
this.spinnerElement.style.borderRadius = "50%";
|
|
167
|
+
this.spinnerElement.style.boxSizing = "border-box";
|
|
168
|
+
this.spinnerElement.style.zIndex = "1000"; // Ensure it's above other elements
|
|
169
|
+
this.spinnerElement.style.display = "block"; // Initially visible
|
|
170
|
+
this.spinnerElement.style.animation = "spin 1s linear infinite";
|
|
171
|
+
|
|
172
|
+
this.imageElement.parentNode.appendChild(this.spinnerElement);
|
|
173
|
+
|
|
174
|
+
// Add CSS keyframes for spinner animation
|
|
175
|
+
if (!document.getElementById("visual-image-tool-spinner-styles")) {
|
|
176
|
+
const styleElement = document.createElement("style");
|
|
177
|
+
styleElement.id = "visual-image-tool-spinner-styles";
|
|
178
|
+
styleElement.innerHTML = `
|
|
179
|
+
@keyframes spin {
|
|
180
|
+
0% { transform: translate(-50%, -50%) rotate(0deg); }
|
|
181
|
+
100% { transform: translate(-50%, -50%) rotate(360deg); }
|
|
182
|
+
}
|
|
183
|
+
`;
|
|
184
|
+
document.head.appendChild(styleElement);
|
|
185
|
+
}
|
|
139
186
|
}
|
|
140
187
|
}
|
|
141
188
|
|
|
142
189
|
/**
|
|
143
|
-
*
|
|
190
|
+
* Updates scaling factors
|
|
144
191
|
* @private
|
|
145
192
|
*/
|
|
146
193
|
_updateScaling() {
|
|
147
194
|
this.state.originalWidth = this.imageElement.naturalWidth || 1;
|
|
148
195
|
this.state.originalHeight = this.imageElement.naturalHeight || 1;
|
|
196
|
+
|
|
197
|
+
// Hide spinner once image is loaded and dimensions are available
|
|
198
|
+
if (
|
|
199
|
+
!this.initialLoadComplete &&
|
|
200
|
+
this.spinnerElement &&
|
|
201
|
+
this.state.originalWidth > 1 &&
|
|
202
|
+
this.state.originalHeight > 1
|
|
203
|
+
) {
|
|
204
|
+
this.spinnerElement.style.display = "none";
|
|
205
|
+
this.initialLoadComplete = true;
|
|
206
|
+
}
|
|
149
207
|
this.state.displayWidth = this.imageElement.offsetWidth;
|
|
150
208
|
this.state.displayHeight = this.imageElement.offsetHeight;
|
|
151
209
|
this.state.scaleX = this.state.displayWidth / this.state.originalWidth;
|
|
152
210
|
this.state.scaleY = this.state.displayHeight / this.state.originalHeight;
|
|
153
211
|
|
|
154
|
-
//
|
|
212
|
+
// Update spinner position during initial load if still visible
|
|
213
|
+
if (!this.initialLoadComplete && this.spinnerElement) {
|
|
214
|
+
this.spinnerElement.style.top = `${this.state.displayHeight / 2}px`;
|
|
215
|
+
this.spinnerElement.style.left = `${this.state.displayWidth / 2}px`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Reposition elements if active
|
|
155
219
|
if (this.state.focusActive) {
|
|
156
220
|
this._updateFocusMarkerPosition();
|
|
157
221
|
}
|
|
@@ -162,11 +226,11 @@ class VisualImageTool {
|
|
|
162
226
|
}
|
|
163
227
|
|
|
164
228
|
/**
|
|
165
|
-
*
|
|
229
|
+
* Converts display coordinates to original coordinates
|
|
166
230
|
* @private
|
|
167
|
-
* @param {number} scaledX -
|
|
168
|
-
* @param {number} scaledY -
|
|
169
|
-
* @returns {Object}
|
|
231
|
+
* @param {number} scaledX - X-coordinate at display scale
|
|
232
|
+
* @param {number} scaledY - Y-coordinate at display scale
|
|
233
|
+
* @returns {Object} Original coordinates {x, y}
|
|
170
234
|
*/
|
|
171
235
|
_toOriginalCoords(scaledX, scaledY) {
|
|
172
236
|
return {
|
|
@@ -176,11 +240,11 @@ class VisualImageTool {
|
|
|
176
240
|
}
|
|
177
241
|
|
|
178
242
|
/**
|
|
179
|
-
*
|
|
243
|
+
* Converts original coordinates to display coordinates
|
|
180
244
|
* @private
|
|
181
|
-
* @param {number} originalX -
|
|
182
|
-
* @param {number} originalY -
|
|
183
|
-
* @returns {Object}
|
|
245
|
+
* @param {number} originalX - Original X-coordinate
|
|
246
|
+
* @param {number} originalY - Original Y-coordinate
|
|
247
|
+
* @returns {Object} Display scale coordinates {x, y}
|
|
184
248
|
*/
|
|
185
249
|
_toScaledCoords(originalX, originalY) {
|
|
186
250
|
return {
|
|
@@ -190,7 +254,7 @@ class VisualImageTool {
|
|
|
190
254
|
}
|
|
191
255
|
|
|
192
256
|
/**
|
|
193
|
-
*
|
|
257
|
+
* Creates the focus point marker
|
|
194
258
|
* @private
|
|
195
259
|
*/
|
|
196
260
|
_createFocusMarker() {
|
|
@@ -213,23 +277,20 @@ class VisualImageTool {
|
|
|
213
277
|
this.imageElement.parentNode.appendChild(marker);
|
|
214
278
|
this.state.focusMarker = marker;
|
|
215
279
|
|
|
216
|
-
//
|
|
217
|
-
marker.addEventListener(
|
|
218
|
-
"mousedown",
|
|
219
|
-
this._handleFocusMarkerMouseDown.bind(this),
|
|
220
|
-
);
|
|
280
|
+
// Add event listeners to the marker
|
|
281
|
+
marker.addEventListener("mousedown", this._boundFocusMarkerMouseDown);
|
|
221
282
|
}
|
|
222
283
|
|
|
223
284
|
/**
|
|
224
|
-
*
|
|
285
|
+
* Handles mousedown event on the focus point marker
|
|
225
286
|
* @private
|
|
226
|
-
* @param {MouseEvent} e -
|
|
287
|
+
* @param {MouseEvent} e - Mousedown event
|
|
227
288
|
*/
|
|
228
289
|
_handleFocusMarkerMouseDown(e) {
|
|
229
290
|
this.interaction.focusDragging = true;
|
|
230
291
|
this.state.focusMarker.style.cursor = "grabbing";
|
|
231
292
|
|
|
232
|
-
//
|
|
293
|
+
// Calculate offset from marker center
|
|
233
294
|
const rect = this.state.focusMarker.getBoundingClientRect();
|
|
234
295
|
this.interaction.focusDragOffsetX =
|
|
235
296
|
e.clientX - (rect.left + rect.width / 2);
|
|
@@ -240,7 +301,7 @@ class VisualImageTool {
|
|
|
240
301
|
}
|
|
241
302
|
|
|
242
303
|
/**
|
|
243
|
-
*
|
|
304
|
+
* Creates the crop zone overlay
|
|
244
305
|
* @private
|
|
245
306
|
*/
|
|
246
307
|
_createCropOverlay() {
|
|
@@ -255,7 +316,7 @@ class VisualImageTool {
|
|
|
255
316
|
overlay.style.display = "none";
|
|
256
317
|
overlay.style.zIndex = "998";
|
|
257
318
|
|
|
258
|
-
//
|
|
319
|
+
// Add resize handles
|
|
259
320
|
const handles = ["tl", "tm", "tr", "ml", "mr", "bl", "bm", "br"];
|
|
260
321
|
for (const handleType of handles) {
|
|
261
322
|
const handle = document.createElement("div");
|
|
@@ -269,7 +330,7 @@ class VisualImageTool {
|
|
|
269
330
|
handle.style.boxSizing = "border-box";
|
|
270
331
|
handle.dataset.handle = handleType;
|
|
271
332
|
|
|
272
|
-
//
|
|
333
|
+
// Position the handle
|
|
273
334
|
switch (handleType) {
|
|
274
335
|
case "tl": // Top-left
|
|
275
336
|
handle.style.top = "-7px";
|
|
@@ -317,35 +378,32 @@ class VisualImageTool {
|
|
|
317
378
|
break;
|
|
318
379
|
}
|
|
319
380
|
|
|
320
|
-
//
|
|
321
|
-
handle.addEventListener("mousedown",
|
|
322
|
-
this._handleCropHandleMouseDown(e, handleType),
|
|
323
|
-
);
|
|
381
|
+
// Add event listener
|
|
382
|
+
handle.addEventListener("mousedown", this._boundCropHandleMouseDown);
|
|
324
383
|
|
|
325
384
|
overlay.appendChild(handle);
|
|
385
|
+
this.cropHandles.push(handle);
|
|
326
386
|
}
|
|
327
387
|
|
|
328
|
-
//
|
|
329
|
-
overlay.addEventListener(
|
|
330
|
-
"mousedown",
|
|
331
|
-
this._handleCropOverlayMouseDown.bind(this),
|
|
332
|
-
);
|
|
388
|
+
// Add listener for overlay dragging
|
|
389
|
+
overlay.addEventListener("mousedown", this._boundCropOverlayMouseDown);
|
|
333
390
|
|
|
334
391
|
this.imageElement.parentNode.appendChild(overlay);
|
|
335
392
|
this.state.cropOverlay = overlay;
|
|
336
393
|
}
|
|
337
394
|
|
|
338
395
|
/**
|
|
339
|
-
*
|
|
396
|
+
* Handles mousedown event on a resize handle
|
|
340
397
|
* @private
|
|
341
|
-
* @param {MouseEvent} e -
|
|
342
|
-
* @param {string} handleType - Type de poignée
|
|
398
|
+
* @param {MouseEvent} e - Mousedown event
|
|
343
399
|
*/
|
|
344
|
-
_handleCropHandleMouseDown(e
|
|
400
|
+
_handleCropHandleMouseDown(e) {
|
|
401
|
+
const handleType = e.currentTarget.dataset.handle;
|
|
402
|
+
|
|
345
403
|
this.interaction.cropResizing = true;
|
|
346
404
|
this.interaction.activeHandle = handleType;
|
|
347
405
|
|
|
348
|
-
//
|
|
406
|
+
// Record initial dimensions and position
|
|
349
407
|
this.interaction.startX =
|
|
350
408
|
Number.parseInt(this.state.cropOverlay.style.left, 10) || 0;
|
|
351
409
|
this.interaction.startY =
|
|
@@ -360,18 +418,18 @@ class VisualImageTool {
|
|
|
360
418
|
}
|
|
361
419
|
|
|
362
420
|
/**
|
|
363
|
-
*
|
|
421
|
+
* Handles mousedown event on the crop overlay
|
|
364
422
|
* @private
|
|
365
|
-
* @param {MouseEvent} e -
|
|
423
|
+
* @param {MouseEvent} e - Mousedown event
|
|
366
424
|
*/
|
|
367
425
|
_handleCropOverlayMouseDown(e) {
|
|
368
|
-
//
|
|
426
|
+
// Ignore if clicking on a handle
|
|
369
427
|
if (e.target !== this.state.cropOverlay) return;
|
|
370
428
|
|
|
371
429
|
this.interaction.cropDragging = true;
|
|
372
430
|
this.state.cropOverlay.style.cursor = "grabbing";
|
|
373
431
|
|
|
374
|
-
//
|
|
432
|
+
// Record initial position
|
|
375
433
|
this.interaction.startX =
|
|
376
434
|
Number.parseInt(this.state.cropOverlay.style.left, 10) || 0;
|
|
377
435
|
this.interaction.startY =
|
|
@@ -383,28 +441,25 @@ class VisualImageTool {
|
|
|
383
441
|
}
|
|
384
442
|
|
|
385
443
|
/**
|
|
386
|
-
*
|
|
444
|
+
* Sets up global event listeners
|
|
387
445
|
* @private
|
|
388
446
|
*/
|
|
389
447
|
_setupEventListeners() {
|
|
390
|
-
//
|
|
391
|
-
window.addEventListener("resize", this.
|
|
448
|
+
// Listener for window resize
|
|
449
|
+
window.addEventListener("resize", this._boundUpdateScaling);
|
|
392
450
|
|
|
393
|
-
//
|
|
451
|
+
// Listener for image load
|
|
394
452
|
if (!this.imageElement.complete) {
|
|
395
|
-
this.imageElement.addEventListener(
|
|
396
|
-
"load",
|
|
397
|
-
this._updateScaling.bind(this),
|
|
398
|
-
);
|
|
453
|
+
this.imageElement.addEventListener("load", this._boundUpdateScaling);
|
|
399
454
|
}
|
|
400
455
|
|
|
401
|
-
//
|
|
402
|
-
document.addEventListener("mouseup", this.
|
|
403
|
-
document.addEventListener("mousemove", this.
|
|
456
|
+
// Listeners for mouse interactions
|
|
457
|
+
document.addEventListener("mouseup", this._boundHandleMouseUp);
|
|
458
|
+
document.addEventListener("mousemove", this._boundHandleMouseMove);
|
|
404
459
|
}
|
|
405
460
|
|
|
406
461
|
/**
|
|
407
|
-
*
|
|
462
|
+
* Handles global mouseup event
|
|
408
463
|
* @private
|
|
409
464
|
*/
|
|
410
465
|
_handleMouseUp() {
|
|
@@ -424,75 +479,75 @@ class VisualImageTool {
|
|
|
424
479
|
}
|
|
425
480
|
|
|
426
481
|
/**
|
|
427
|
-
*
|
|
482
|
+
* Handles global mousemove event
|
|
428
483
|
* @private
|
|
429
|
-
* @param {MouseEvent} e -
|
|
484
|
+
* @param {MouseEvent} e - Mousemove event
|
|
430
485
|
*/
|
|
431
486
|
_handleMouseMove(e) {
|
|
432
|
-
//
|
|
487
|
+
// Handle focus point dragging
|
|
433
488
|
if (this.interaction.focusDragging && this.state.focusMarker) {
|
|
434
489
|
this._handleFocusMarkerDrag(e);
|
|
435
490
|
}
|
|
436
491
|
|
|
437
|
-
//
|
|
492
|
+
// Handle crop zone dragging
|
|
438
493
|
if (this.interaction.cropDragging) {
|
|
439
494
|
this._handleCropOverlayDrag(e);
|
|
440
495
|
}
|
|
441
496
|
|
|
442
|
-
//
|
|
497
|
+
// Handle crop zone resizing
|
|
443
498
|
if (this.interaction.cropResizing) {
|
|
444
499
|
this._handleCropOverlayResize(e);
|
|
445
500
|
}
|
|
446
501
|
}
|
|
447
502
|
|
|
448
503
|
/**
|
|
449
|
-
*
|
|
504
|
+
* Handles focus point marker drag
|
|
450
505
|
* @private
|
|
451
|
-
* @param {MouseEvent} e -
|
|
506
|
+
* @param {MouseEvent} e - Mousemove event
|
|
452
507
|
*/
|
|
453
508
|
_handleFocusMarkerDrag(e) {
|
|
454
509
|
const imageRect = this.imageElement.getBoundingClientRect();
|
|
455
510
|
|
|
456
|
-
//
|
|
511
|
+
// Calculate target position relative to the image
|
|
457
512
|
const targetScaledX =
|
|
458
513
|
e.clientX - imageRect.left - this.interaction.focusDragOffsetX;
|
|
459
514
|
const targetScaledY =
|
|
460
515
|
e.clientY - imageRect.top - this.interaction.focusDragOffsetY;
|
|
461
516
|
|
|
462
|
-
//
|
|
517
|
+
// Convert to original coordinates
|
|
463
518
|
const original = this._toOriginalCoords(targetScaledX, targetScaledY);
|
|
464
519
|
|
|
465
|
-
//
|
|
520
|
+
// Update focus point
|
|
466
521
|
this.setFocusPoint(original.x, original.y);
|
|
467
522
|
}
|
|
468
523
|
|
|
469
524
|
/**
|
|
470
|
-
*
|
|
525
|
+
* Handles crop overlay drag
|
|
471
526
|
* @private
|
|
472
|
-
* @param {MouseEvent} e -
|
|
527
|
+
* @param {MouseEvent} e - Mousemove event
|
|
473
528
|
*/
|
|
474
529
|
_handleCropOverlayDrag(e) {
|
|
475
530
|
const deltaX = e.clientX - this.interaction.startMouseX;
|
|
476
531
|
const deltaY = e.clientY - this.interaction.startMouseY;
|
|
477
532
|
|
|
478
|
-
//
|
|
533
|
+
// Calculate new position in display pixels
|
|
479
534
|
const newX = this.interaction.startX + deltaX;
|
|
480
535
|
const newY = this.interaction.startY + deltaY;
|
|
481
536
|
|
|
482
|
-
//
|
|
537
|
+
// Convert to original coordinates
|
|
483
538
|
const original = this._toOriginalCoords(newX, newY);
|
|
484
539
|
|
|
485
|
-
//
|
|
540
|
+
// Get current dimensions in original coordinates
|
|
486
541
|
const { width, height } = this.state.cropZone;
|
|
487
542
|
|
|
488
|
-
//
|
|
543
|
+
// Update crop zone
|
|
489
544
|
this.setCropZone(original.x, original.y, width, height);
|
|
490
545
|
}
|
|
491
546
|
|
|
492
547
|
/**
|
|
493
|
-
*
|
|
548
|
+
* Handles crop overlay resize
|
|
494
549
|
* @private
|
|
495
|
-
* @param {MouseEvent} e -
|
|
550
|
+
* @param {MouseEvent} e - Mousemove event
|
|
496
551
|
*/
|
|
497
552
|
_handleCropOverlayResize(e) {
|
|
498
553
|
if (!this.interaction.activeHandle) return;
|
|
@@ -505,7 +560,7 @@ class VisualImageTool {
|
|
|
505
560
|
let newWidth = this.interaction.startWidth;
|
|
506
561
|
let newHeight = this.interaction.startHeight;
|
|
507
562
|
|
|
508
|
-
//
|
|
563
|
+
// Adjust based on active handle
|
|
509
564
|
if (this.interaction.activeHandle.includes("t")) {
|
|
510
565
|
// Top
|
|
511
566
|
newY = this.interaction.startY + deltaY;
|
|
@@ -525,7 +580,7 @@ class VisualImageTool {
|
|
|
525
580
|
newWidth = this.interaction.startWidth + deltaX;
|
|
526
581
|
}
|
|
527
582
|
|
|
528
|
-
//
|
|
583
|
+
// Prevent negative dimensions
|
|
529
584
|
if (newWidth < 10) {
|
|
530
585
|
if (this.interaction.activeHandle.includes("l")) {
|
|
531
586
|
newX = this.interaction.startX + this.interaction.startWidth - 10;
|
|
@@ -539,18 +594,18 @@ class VisualImageTool {
|
|
|
539
594
|
newHeight = 10;
|
|
540
595
|
}
|
|
541
596
|
|
|
542
|
-
//
|
|
597
|
+
// Convert display coordinates to original coordinates
|
|
543
598
|
const originalX = newX / this.state.scaleX;
|
|
544
599
|
const originalY = newY / this.state.scaleY;
|
|
545
600
|
const originalWidth = newWidth / this.state.scaleX;
|
|
546
601
|
const originalHeight = newHeight / this.state.scaleY;
|
|
547
602
|
|
|
548
|
-
//
|
|
603
|
+
// Update crop zone
|
|
549
604
|
this.setCropZone(originalX, originalY, originalWidth, originalHeight);
|
|
550
605
|
}
|
|
551
606
|
|
|
552
607
|
/**
|
|
553
|
-
*
|
|
608
|
+
* Updates the focus point marker position
|
|
554
609
|
* @private
|
|
555
610
|
*/
|
|
556
611
|
_updateFocusMarkerPosition() {
|
|
@@ -558,11 +613,11 @@ class VisualImageTool {
|
|
|
558
613
|
|
|
559
614
|
const { x, y } = this.state.focusPoint;
|
|
560
615
|
|
|
561
|
-
//
|
|
616
|
+
// Clamp coordinates to image dimensions
|
|
562
617
|
const clampedX = Math.max(0, Math.min(x, this.state.originalWidth));
|
|
563
618
|
const clampedY = Math.max(0, Math.min(y, this.state.originalHeight));
|
|
564
619
|
|
|
565
|
-
//
|
|
620
|
+
// Update state if coordinates were clamped
|
|
566
621
|
if (x !== clampedX || y !== clampedY) {
|
|
567
622
|
this.state.focusPoint = { x: clampedX, y: clampedY };
|
|
568
623
|
}
|
|
@@ -587,7 +642,7 @@ class VisualImageTool {
|
|
|
587
642
|
);
|
|
588
643
|
}
|
|
589
644
|
|
|
590
|
-
//
|
|
645
|
+
// Adjust to center the marker
|
|
591
646
|
// Adjust for container padding (use unique variable names)
|
|
592
647
|
let focusPaddingLeft = 0;
|
|
593
648
|
let focusPaddingTop = 0;
|
|
@@ -615,7 +670,7 @@ class VisualImageTool {
|
|
|
615
670
|
}
|
|
616
671
|
|
|
617
672
|
/**
|
|
618
|
-
*
|
|
673
|
+
* Updates the crop overlay position and dimensions
|
|
619
674
|
* @private
|
|
620
675
|
*/
|
|
621
676
|
_updateCropOverlayPosition() {
|
|
@@ -623,7 +678,7 @@ class VisualImageTool {
|
|
|
623
678
|
|
|
624
679
|
const { x, y, width, height } = this.state.cropZone;
|
|
625
680
|
|
|
626
|
-
//
|
|
681
|
+
// Clamp to image dimensions
|
|
627
682
|
const clampedX = Math.max(0, Math.min(x, this.state.originalWidth - width));
|
|
628
683
|
const clampedY = Math.max(
|
|
629
684
|
0,
|
|
@@ -638,7 +693,7 @@ class VisualImageTool {
|
|
|
638
693
|
Math.min(height, this.state.originalHeight - clampedY),
|
|
639
694
|
);
|
|
640
695
|
|
|
641
|
-
//
|
|
696
|
+
// Update state if values were clamped
|
|
642
697
|
if (
|
|
643
698
|
x !== clampedX ||
|
|
644
699
|
y !== clampedY ||
|
|
@@ -653,7 +708,7 @@ class VisualImageTool {
|
|
|
653
708
|
};
|
|
654
709
|
}
|
|
655
710
|
|
|
656
|
-
//
|
|
711
|
+
// Convert to display coordinates
|
|
657
712
|
const scaled = this._toScaledCoords(clampedX, clampedY);
|
|
658
713
|
const scaledWidth = clampedWidth * this.state.scaleX;
|
|
659
714
|
const scaledHeight = clampedHeight * this.state.scaleY;
|
|
@@ -676,7 +731,7 @@ class VisualImageTool {
|
|
|
676
731
|
);
|
|
677
732
|
}
|
|
678
733
|
|
|
679
|
-
//
|
|
734
|
+
// Update overlay
|
|
680
735
|
// Adjust for container padding (use unique variable names)
|
|
681
736
|
let cropPaddingLeft = 0;
|
|
682
737
|
let cropPaddingTop = 0;
|
|
@@ -702,10 +757,11 @@ class VisualImageTool {
|
|
|
702
757
|
}
|
|
703
758
|
|
|
704
759
|
/**
|
|
705
|
-
*
|
|
760
|
+
* Enables or disables the focus point.
|
|
761
|
+
* If the focus point is at its initial state `{x:0, y:0}` (top-left corner), it will be moved to the center of the image when this method is called with `active = true` for the first time.
|
|
706
762
|
* @public
|
|
707
|
-
* @param {boolean} active -
|
|
708
|
-
* @returns {
|
|
763
|
+
* @param {boolean} active - Activation state
|
|
764
|
+
* @returns {VisualImageTool} Instance for chaining
|
|
709
765
|
*/
|
|
710
766
|
toggleFocusPoint(active) {
|
|
711
767
|
if (!this.options.focusPoint.enabled) return this;
|
|
@@ -713,12 +769,12 @@ class VisualImageTool {
|
|
|
713
769
|
const isActive = active === undefined ? !this.state.focusActive : active;
|
|
714
770
|
|
|
715
771
|
if (isActive && !this.state.focusActive) {
|
|
716
|
-
//
|
|
772
|
+
// Enable focus point
|
|
717
773
|
if (!this.state.focusMarker) {
|
|
718
774
|
this._createFocusMarker();
|
|
719
775
|
}
|
|
720
776
|
|
|
721
|
-
//
|
|
777
|
+
// Position at image center by default if not already set
|
|
722
778
|
if (this.state.focusPoint.x === 0 && this.state.focusPoint.y === 0) {
|
|
723
779
|
this.state.focusPoint = {
|
|
724
780
|
x: this.state.originalWidth / 2,
|
|
@@ -730,24 +786,24 @@ class VisualImageTool {
|
|
|
730
786
|
this.state.focusMarker.style.display = "block";
|
|
731
787
|
this.state.focusActive = true;
|
|
732
788
|
} else if (!isActive && this.state.focusActive) {
|
|
733
|
-
//
|
|
789
|
+
// Disable focus point
|
|
734
790
|
if (this.state.focusMarker) {
|
|
735
791
|
this.state.focusMarker.style.display = "none";
|
|
736
792
|
}
|
|
737
793
|
this.state.focusActive = false;
|
|
738
794
|
}
|
|
739
795
|
|
|
740
|
-
//
|
|
796
|
+
// Notify change
|
|
741
797
|
this._notifyChange();
|
|
742
798
|
|
|
743
799
|
return this;
|
|
744
800
|
}
|
|
745
801
|
|
|
746
802
|
/**
|
|
747
|
-
*
|
|
803
|
+
* Enables or disables the crop zone
|
|
748
804
|
* @public
|
|
749
|
-
* @param {boolean} active -
|
|
750
|
-
* @returns {
|
|
805
|
+
* @param {boolean} active - Activation state
|
|
806
|
+
* @returns {VisualImageTool} Instance for chaining
|
|
751
807
|
*/
|
|
752
808
|
toggleCropZone(active) {
|
|
753
809
|
if (!this.options.cropZone.enabled) return this;
|
|
@@ -755,12 +811,12 @@ class VisualImageTool {
|
|
|
755
811
|
const isActive = active === undefined ? !this.state.cropActive : active;
|
|
756
812
|
|
|
757
813
|
if (isActive && !this.state.cropActive) {
|
|
758
|
-
//
|
|
814
|
+
// Enable crop zone
|
|
759
815
|
if (!this.state.cropOverlay) {
|
|
760
816
|
this._createCropOverlay();
|
|
761
817
|
}
|
|
762
818
|
|
|
763
|
-
//
|
|
819
|
+
// Define a default zone if not already set
|
|
764
820
|
if (this.state.cropZone.width === 0 || this.state.cropZone.height === 0) {
|
|
765
821
|
const defaultWidth = this.state.originalWidth / 2;
|
|
766
822
|
const defaultHeight = this.state.originalHeight / 2;
|
|
@@ -779,28 +835,28 @@ class VisualImageTool {
|
|
|
779
835
|
this.state.cropOverlay.style.display = "block";
|
|
780
836
|
this.state.cropActive = true;
|
|
781
837
|
} else if (!isActive && this.state.cropActive) {
|
|
782
|
-
//
|
|
838
|
+
// Disable crop zone
|
|
783
839
|
if (this.state.cropOverlay) {
|
|
784
840
|
this.state.cropOverlay.style.display = "none";
|
|
785
841
|
}
|
|
786
842
|
this.state.cropActive = false;
|
|
787
843
|
}
|
|
788
844
|
|
|
789
|
-
//
|
|
845
|
+
// Notify change
|
|
790
846
|
this._notifyChange();
|
|
791
847
|
|
|
792
848
|
return this;
|
|
793
849
|
}
|
|
794
850
|
|
|
795
851
|
/**
|
|
796
|
-
*
|
|
852
|
+
* Sets the focus point position
|
|
797
853
|
* @public
|
|
798
|
-
* @param {number} x -
|
|
799
|
-
* @param {number} y -
|
|
800
|
-
* @returns {
|
|
854
|
+
* @param {number} x - X-coordinate in original pixels
|
|
855
|
+
* @param {number} y - Y-coordinate in original pixels
|
|
856
|
+
* @returns {VisualImageTool} Instance for chaining
|
|
801
857
|
*/
|
|
802
858
|
setFocusPoint(x, y) {
|
|
803
|
-
//
|
|
859
|
+
// Clamp coordinates to image dimensions
|
|
804
860
|
const clampedX = Math.max(0, Math.min(x, this.state.originalWidth));
|
|
805
861
|
const clampedY = Math.max(0, Math.min(y, this.state.originalHeight));
|
|
806
862
|
|
|
@@ -810,23 +866,23 @@ class VisualImageTool {
|
|
|
810
866
|
this._updateFocusMarkerPosition();
|
|
811
867
|
}
|
|
812
868
|
|
|
813
|
-
//
|
|
869
|
+
// Notify change
|
|
814
870
|
this._notifyChange();
|
|
815
871
|
|
|
816
872
|
return this;
|
|
817
873
|
}
|
|
818
874
|
|
|
819
875
|
/**
|
|
820
|
-
*
|
|
876
|
+
* Sets the crop zone position and dimensions
|
|
821
877
|
* @public
|
|
822
|
-
* @param {number} x -
|
|
823
|
-
* @param {number} y -
|
|
824
|
-
* @param {number} width -
|
|
825
|
-
* @param {number} height -
|
|
826
|
-
* @returns {
|
|
878
|
+
* @param {number} x - X-coordinate in original pixels
|
|
879
|
+
* @param {number} y - Y-coordinate in original pixels
|
|
880
|
+
* @param {number} width - Width in original pixels
|
|
881
|
+
* @param {number} height - Height in original pixels
|
|
882
|
+
* @returns {VisualImageTool} Instance for chaining
|
|
827
883
|
*/
|
|
828
884
|
setCropZone(x, y, width, height) {
|
|
829
|
-
//
|
|
885
|
+
// Clamp to image dimensions
|
|
830
886
|
const clampedX = Math.max(0, Math.min(x, this.state.originalWidth - width));
|
|
831
887
|
const clampedY = Math.max(
|
|
832
888
|
0,
|
|
@@ -852,32 +908,32 @@ class VisualImageTool {
|
|
|
852
908
|
this._updateCropOverlayPosition();
|
|
853
909
|
}
|
|
854
910
|
|
|
855
|
-
//
|
|
911
|
+
// Notify change
|
|
856
912
|
this._notifyChange();
|
|
857
913
|
|
|
858
914
|
return this;
|
|
859
915
|
}
|
|
860
916
|
|
|
861
917
|
/**
|
|
862
|
-
*
|
|
918
|
+
* Gets the current focus point position
|
|
863
919
|
* @public
|
|
864
|
-
* @returns {Object}
|
|
920
|
+
* @returns {Object} Focus point coordinates {x, y}
|
|
865
921
|
*/
|
|
866
922
|
getFocusPoint() {
|
|
867
923
|
return { ...this.state.focusPoint };
|
|
868
924
|
}
|
|
869
925
|
|
|
870
926
|
/**
|
|
871
|
-
*
|
|
927
|
+
* Gets the current crop zone position and dimensions
|
|
872
928
|
* @public
|
|
873
|
-
* @returns {Object}
|
|
929
|
+
* @returns {Object} Crop zone {x, y, width, height}
|
|
874
930
|
*/
|
|
875
931
|
getCropZone() {
|
|
876
932
|
return { ...this.state.cropZone };
|
|
877
933
|
}
|
|
878
934
|
|
|
879
935
|
/**
|
|
880
|
-
*
|
|
936
|
+
* Gets the original image dimensions
|
|
881
937
|
* @public
|
|
882
938
|
* @returns {Object} Dimensions {width, height}
|
|
883
939
|
*/
|
|
@@ -889,7 +945,7 @@ class VisualImageTool {
|
|
|
889
945
|
}
|
|
890
946
|
|
|
891
947
|
/**
|
|
892
|
-
*
|
|
948
|
+
* Notifies changes via callback
|
|
893
949
|
* @private
|
|
894
950
|
*/
|
|
895
951
|
_notifyChange() {
|
|
@@ -904,11 +960,36 @@ class VisualImageTool {
|
|
|
904
960
|
}
|
|
905
961
|
|
|
906
962
|
/**
|
|
907
|
-
*
|
|
963
|
+
* Destroys the instance and cleans up resources
|
|
908
964
|
* @public
|
|
909
965
|
*/
|
|
910
966
|
destroy() {
|
|
911
|
-
//
|
|
967
|
+
// Already destroyed
|
|
968
|
+
if (!this.state) {
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Remove element listeners
|
|
973
|
+
if (this.state.focusMarker) {
|
|
974
|
+
this.state.focusMarker.removeEventListener(
|
|
975
|
+
"mousedown",
|
|
976
|
+
this._boundFocusMarkerMouseDown,
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
if (this.state.cropOverlay) {
|
|
981
|
+
this.state.cropOverlay.removeEventListener(
|
|
982
|
+
"mousedown",
|
|
983
|
+
this._boundCropOverlayMouseDown,
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
for (const handle of this.cropHandles) {
|
|
988
|
+
handle.removeEventListener("mousedown", this._boundCropHandleMouseDown);
|
|
989
|
+
}
|
|
990
|
+
this.cropHandles = [];
|
|
991
|
+
|
|
992
|
+
// Remove DOM elements
|
|
912
993
|
if (this.state.focusMarker?.parentNode) {
|
|
913
994
|
this.state.focusMarker.parentNode.removeChild(this.state.focusMarker);
|
|
914
995
|
}
|
|
@@ -917,12 +998,29 @@ class VisualImageTool {
|
|
|
917
998
|
this.state.cropOverlay.parentNode.removeChild(this.state.cropOverlay);
|
|
918
999
|
}
|
|
919
1000
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
1001
|
+
if (this.spinnerElement?.parentNode) {
|
|
1002
|
+
this.spinnerElement.parentNode.removeChild(this.spinnerElement);
|
|
1003
|
+
}
|
|
1004
|
+
this.spinnerElement = null;
|
|
1005
|
+
|
|
1006
|
+
// Remove global listeners
|
|
1007
|
+
window.removeEventListener("resize", this._boundUpdateScaling);
|
|
1008
|
+
document.removeEventListener("mouseup", this._boundHandleMouseUp);
|
|
1009
|
+
document.removeEventListener("mousemove", this._boundHandleMouseMove);
|
|
1010
|
+
|
|
1011
|
+
if (this.imageElement) {
|
|
1012
|
+
this.imageElement.removeEventListener("load", this._boundUpdateScaling);
|
|
1013
|
+
}
|
|
924
1014
|
|
|
925
|
-
//
|
|
1015
|
+
// Release bound handlers
|
|
1016
|
+
this._boundUpdateScaling = null;
|
|
1017
|
+
this._boundHandleMouseUp = null;
|
|
1018
|
+
this._boundHandleMouseMove = null;
|
|
1019
|
+
this._boundFocusMarkerMouseDown = null;
|
|
1020
|
+
this._boundCropOverlayMouseDown = null;
|
|
1021
|
+
this._boundCropHandleMouseDown = null;
|
|
1022
|
+
|
|
1023
|
+
// Reset state
|
|
926
1024
|
this.state = null;
|
|
927
1025
|
this.interaction = null;
|
|
928
1026
|
this.options = null;
|
|
@@ -930,11 +1028,5 @@ class VisualImageTool {
|
|
|
930
1028
|
}
|
|
931
1029
|
}
|
|
932
1030
|
|
|
933
|
-
/**
|
|
934
|
-
* Point d'entrée principal pour le package image-tool
|
|
935
|
-
* Exporte la classe VisualImageTool
|
|
936
|
-
*/
|
|
937
|
-
|
|
938
1031
|
exports.VisualImageTool = VisualImageTool;
|
|
939
|
-
|
|
940
|
-
//# sourceMappingURL=visual-image-tool.js.map
|
|
1032
|
+
//# sourceMappingURL=visual-image-tool.cjs.map
|