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