@h4md1/visual-image-tool 0.1.5

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.
@@ -0,0 +1,812 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ /**
6
+ * ImageTool - Un outil léger pour définir des points focaux et zones de recadrage sur des images
7
+ * @module visual-image-tool
8
+ */
9
+
10
+ class VisualImageTool {
11
+ /**
12
+ * Crée une instance de l'outil d'image
13
+ * @param {Object} options - Options de configuration
14
+ * @param {HTMLElement|string} options.imageElement - Élément image ou sélecteur CSS
15
+ * @param {Object} [options.focusPoint] - Configuration du point focal
16
+ * @param {boolean} [options.focusPoint.enabled=true] - Activer la fonctionnalité de point focal
17
+ * @param {Object} [options.focusPoint.style] - Styles personnalisés pour le marqueur de point focal
18
+ * @param {Object} [options.cropZone] - Configuration de la zone de recadrage
19
+ * @param {boolean} [options.cropZone.enabled=true] - Activer la fonctionnalité de zone de recadrage
20
+ * @param {Object} [options.cropZone.style] - Styles personnalisés pour l'overlay de recadrage
21
+ * @param {Function} [options.onChange] - Callback appelé lors des changements
22
+ */
23
+ constructor(options) {
24
+ // Valider les options
25
+ if (!options || !options.imageElement) {
26
+ throw new Error('L\'élément image est requis');
27
+ }
28
+
29
+ // Initialiser les propriétés
30
+ this.imageElement = typeof options.imageElement === 'string'
31
+ ? document.querySelector(options.imageElement)
32
+ : options.imageElement;
33
+
34
+ if (!this.imageElement || this.imageElement.tagName !== 'IMG') {
35
+ throw new Error('Élément image invalide');
36
+ }
37
+
38
+ // Options par défaut
39
+ this.options = {
40
+ focusPoint: {
41
+ enabled: true,
42
+ style: {
43
+ width: '30px',
44
+ height: '30px',
45
+ border: '3px solid white',
46
+ boxShadow: '0 0 0 2px black, 0 0 5px rgba(0,0,0,0.5)',
47
+ backgroundColor: 'rgba(255, 0, 0, 0.5)'
48
+ },
49
+ ...options.focusPoint
50
+ },
51
+ cropZone: {
52
+ enabled: true,
53
+ style: {
54
+ border: '1px dashed #fff',
55
+ backgroundColor: 'rgba(0, 0, 0, 0.4)'
56
+ },
57
+ handleStyle: {
58
+ width: '14px',
59
+ height: '14px',
60
+ backgroundColor: 'white',
61
+ border: '2px solid black',
62
+ boxShadow: '0 0 3px rgba(0,0,0,0.5)'
63
+ },
64
+ ...options.cropZone
65
+ },
66
+ onChange: options.onChange || (() => {})
67
+ };
68
+
69
+ // État interne
70
+ this.state = {
71
+ focusMarker: null,
72
+ cropOverlay: null,
73
+ focusActive: false,
74
+ cropActive: false,
75
+ focusPoint: { x: 0, y: 0 },
76
+ cropZone: { x: 0, y: 0, width: 0, height: 0 },
77
+ originalWidth: 1,
78
+ originalHeight: 1,
79
+ displayWidth: 1,
80
+ displayHeight: 1,
81
+ scaleX: 1,
82
+ scaleY: 1
83
+ };
84
+
85
+ // Variables pour le suivi des interactions
86
+ this.interaction = {
87
+ focusDragging: false,
88
+ focusDragOffsetX: 0,
89
+ focusDragOffsetY: 0,
90
+ cropDragging: false,
91
+ cropResizing: false,
92
+ activeHandle: null,
93
+ startX: 0,
94
+ startY: 0,
95
+ startWidth: 0,
96
+ startHeight: 0,
97
+ startMouseX: 0,
98
+ startMouseY: 0
99
+ };
100
+
101
+ // Initialiser l'outil
102
+ this._init();
103
+ }
104
+
105
+ /**
106
+ * Initialise l'outil d'image
107
+ * @private
108
+ */
109
+ _init() {
110
+ // Préparer le conteneur parent
111
+ this._prepareContainer();
112
+
113
+ // Initialiser les dimensions
114
+ this._updateScaling();
115
+
116
+ // Créer les éléments d'interface si activés
117
+ if (this.options.focusPoint.enabled) {
118
+ this._createFocusMarker();
119
+ }
120
+
121
+ if (this.options.cropZone.enabled) {
122
+ this._createCropOverlay();
123
+ }
124
+
125
+ // Ajouter les écouteurs d'événements
126
+ this._setupEventListeners();
127
+ }
128
+
129
+ /**
130
+ * Prépare le conteneur parent de l'image
131
+ * @private
132
+ */
133
+ _prepareContainer() {
134
+ // S'assurer que le parent est positionné
135
+ if (this.imageElement.parentNode) {
136
+ this.imageElement.parentNode.style.position = 'relative';
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Met à jour les facteurs d'échelle
142
+ * @private
143
+ */
144
+ _updateScaling() {
145
+ this.state.originalWidth = this.imageElement.naturalWidth || 1;
146
+ this.state.originalHeight = this.imageElement.naturalHeight || 1;
147
+ this.state.displayWidth = this.imageElement.offsetWidth;
148
+ this.state.displayHeight = this.imageElement.offsetHeight;
149
+ this.state.scaleX = this.state.displayWidth / this.state.originalWidth;
150
+ this.state.scaleY = this.state.displayHeight / this.state.originalHeight;
151
+
152
+ // Repositionner les éléments si actifs
153
+ if (this.state.focusActive) {
154
+ this._updateFocusMarkerPosition();
155
+ }
156
+
157
+ if (this.state.cropActive) {
158
+ this._updateCropOverlayPosition();
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Convertit des coordonnées d'affichage en coordonnées originales
164
+ * @private
165
+ * @param {number} scaledX - Coordonnée X à l'échelle d'affichage
166
+ * @param {number} scaledY - Coordonnée Y à l'échelle d'affichage
167
+ * @returns {Object} Coordonnées originales
168
+ */
169
+ _toOriginalCoords(scaledX, scaledY) {
170
+ return {
171
+ x: scaledX / this.state.scaleX,
172
+ y: scaledY / this.state.scaleY
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Convertit des coordonnées originales en coordonnées d'affichage
178
+ * @private
179
+ * @param {number} originalX - Coordonnée X originale
180
+ * @param {number} originalY - Coordonnée Y originale
181
+ * @returns {Object} Coordonnées à l'échelle d'affichage
182
+ */
183
+ _toScaledCoords(originalX, originalY) {
184
+ return {
185
+ x: originalX * this.state.scaleX,
186
+ y: originalY * this.state.scaleY
187
+ };
188
+ }
189
+
190
+ /**
191
+ * Crée le marqueur de point focal
192
+ * @private
193
+ */
194
+ _createFocusMarker() {
195
+ if (this.state.focusMarker) return;
196
+
197
+ const marker = document.createElement('div');
198
+ marker.style.position = 'absolute';
199
+ marker.style.width = this.options.focusPoint.style.width;
200
+ marker.style.height = this.options.focusPoint.style.height;
201
+ marker.style.border = this.options.focusPoint.style.border;
202
+ marker.style.boxShadow = this.options.focusPoint.style.boxShadow;
203
+ marker.style.backgroundColor = this.options.focusPoint.style.backgroundColor;
204
+ marker.style.cursor = 'move';
205
+ marker.style.display = 'none';
206
+ marker.style.zIndex = '999';
207
+ marker.style.clipPath = 'polygon(40% 0%, 60% 0%, 60% 40%, 100% 40%, 100% 60%, 60% 60%, 60% 100%, 40% 100%, 40% 60%, 0% 60%, 0% 40%, 40% 40%)';
208
+
209
+ this.imageElement.parentNode.appendChild(marker);
210
+ this.state.focusMarker = marker;
211
+
212
+ // Ajouter les écouteurs d'événements au marqueur
213
+ marker.addEventListener('mousedown', this._handleFocusMarkerMouseDown.bind(this));
214
+ }
215
+
216
+ /**
217
+ * Gère l'événement mousedown sur le marqueur de point focal
218
+ * @private
219
+ * @param {MouseEvent} e - Événement mousedown
220
+ */
221
+ _handleFocusMarkerMouseDown(e) {
222
+ this.interaction.focusDragging = true;
223
+ this.state.focusMarker.style.cursor = 'grabbing';
224
+
225
+ // Calculer le décalage par rapport au centre du marqueur
226
+ const rect = this.state.focusMarker.getBoundingClientRect();
227
+ this.interaction.focusDragOffsetX = e.clientX - (rect.left + rect.width / 2);
228
+ this.interaction.focusDragOffsetY = e.clientY - (rect.top + rect.height / 2);
229
+
230
+ e.preventDefault();
231
+ }
232
+
233
+ /**
234
+ * Crée l'overlay de zone de recadrage
235
+ * @private
236
+ */
237
+ _createCropOverlay() {
238
+ if (this.state.cropOverlay) return;
239
+
240
+ const overlay = document.createElement('div');
241
+ overlay.style.position = 'absolute';
242
+ overlay.style.border = this.options.cropZone.style.border;
243
+ overlay.style.backgroundColor = this.options.cropZone.style.backgroundColor;
244
+ overlay.style.boxSizing = 'border-box';
245
+ overlay.style.cursor = 'move';
246
+ overlay.style.display = 'none';
247
+ overlay.style.zIndex = '998';
248
+
249
+ // Ajouter les poignées de redimensionnement
250
+ const handles = ['tl', 'tm', 'tr', 'ml', 'mr', 'bl', 'bm', 'br'];
251
+ handles.forEach(handleType => {
252
+ const handle = document.createElement('div');
253
+ handle.style.position = 'absolute';
254
+ handle.style.width = this.options.cropZone.handleStyle.width;
255
+ handle.style.height = this.options.cropZone.handleStyle.height;
256
+ handle.style.backgroundColor = this.options.cropZone.handleStyle.backgroundColor;
257
+ handle.style.border = this.options.cropZone.handleStyle.border;
258
+ handle.style.boxShadow = this.options.cropZone.handleStyle.boxShadow;
259
+ handle.style.boxSizing = 'border-box';
260
+ handle.dataset.handle = handleType;
261
+
262
+ // Positionner la poignée
263
+ switch (handleType) {
264
+ case 'tl': // Top-left
265
+ handle.style.top = '-7px';
266
+ handle.style.left = '-7px';
267
+ handle.style.cursor = 'nwse-resize';
268
+ break;
269
+ case 'tm': // Top-middle
270
+ handle.style.top = '-7px';
271
+ handle.style.left = '50%';
272
+ handle.style.marginLeft = '-7px';
273
+ handle.style.cursor = 'ns-resize';
274
+ break;
275
+ case 'tr': // Top-right
276
+ handle.style.top = '-7px';
277
+ handle.style.right = '-7px';
278
+ handle.style.cursor = 'nesw-resize';
279
+ break;
280
+ case 'ml': // Middle-left
281
+ handle.style.top = '50%';
282
+ handle.style.left = '-7px';
283
+ handle.style.marginTop = '-7px';
284
+ handle.style.cursor = 'ew-resize';
285
+ break;
286
+ case 'mr': // Middle-right
287
+ handle.style.top = '50%';
288
+ handle.style.right = '-7px';
289
+ handle.style.marginTop = '-7px';
290
+ handle.style.cursor = 'ew-resize';
291
+ break;
292
+ case 'bl': // Bottom-left
293
+ handle.style.bottom = '-7px';
294
+ handle.style.left = '-7px';
295
+ handle.style.cursor = 'nesw-resize';
296
+ break;
297
+ case 'bm': // Bottom-middle
298
+ handle.style.bottom = '-7px';
299
+ handle.style.left = '50%';
300
+ handle.style.marginLeft = '-7px';
301
+ handle.style.cursor = 'ns-resize';
302
+ break;
303
+ case 'br': // Bottom-right
304
+ handle.style.bottom = '-7px';
305
+ handle.style.right = '-7px';
306
+ handle.style.cursor = 'nwse-resize';
307
+ break;
308
+ }
309
+
310
+ // Ajouter l'écouteur d'événement
311
+ handle.addEventListener('mousedown', (e) => this._handleCropHandleMouseDown(e, handleType));
312
+
313
+ overlay.appendChild(handle);
314
+ });
315
+
316
+ // Ajouter l'écouteur pour le déplacement de l'overlay
317
+ overlay.addEventListener('mousedown', this._handleCropOverlayMouseDown.bind(this));
318
+
319
+ this.imageElement.parentNode.appendChild(overlay);
320
+ this.state.cropOverlay = overlay;
321
+ }
322
+
323
+ /**
324
+ * Gère l'événement mousedown sur une poignée de redimensionnement
325
+ * @private
326
+ * @param {MouseEvent} e - Événement mousedown
327
+ * @param {string} handleType - Type de poignée
328
+ */
329
+ _handleCropHandleMouseDown(e, handleType) {
330
+ this.interaction.cropResizing = true;
331
+ this.interaction.activeHandle = handleType;
332
+
333
+ // Enregistrer les dimensions et position initiales
334
+ this.interaction.startX = parseInt(this.state.cropOverlay.style.left, 10) || 0;
335
+ this.interaction.startY = parseInt(this.state.cropOverlay.style.top, 10) || 0;
336
+ this.interaction.startWidth = this.state.cropOverlay.offsetWidth;
337
+ this.interaction.startHeight = this.state.cropOverlay.offsetHeight;
338
+ this.interaction.startMouseX = e.clientX;
339
+ this.interaction.startMouseY = e.clientY;
340
+
341
+ e.preventDefault();
342
+ e.stopPropagation();
343
+ }
344
+
345
+ /**
346
+ * Gère l'événement mousedown sur l'overlay de recadrage
347
+ * @private
348
+ * @param {MouseEvent} e - Événement mousedown
349
+ */
350
+ _handleCropOverlayMouseDown(e) {
351
+ // Ignorer si on clique sur une poignée
352
+ if (e.target !== this.state.cropOverlay) return;
353
+
354
+ this.interaction.cropDragging = true;
355
+ this.state.cropOverlay.style.cursor = 'grabbing';
356
+
357
+ // Enregistrer la position initiale
358
+ this.interaction.startX = parseInt(this.state.cropOverlay.style.left, 10) || 0;
359
+ this.interaction.startY = parseInt(this.state.cropOverlay.style.top, 10) || 0;
360
+ this.interaction.startMouseX = e.clientX;
361
+ this.interaction.startMouseY = e.clientY;
362
+
363
+ e.preventDefault();
364
+ }
365
+
366
+ /**
367
+ * Configure les écouteurs d'événements globaux
368
+ * @private
369
+ */
370
+ _setupEventListeners() {
371
+ // Écouteur pour le redimensionnement de la fenêtre
372
+ window.addEventListener('resize', this._updateScaling.bind(this));
373
+
374
+ // Écouteur pour le chargement de l'image
375
+ if (!this.imageElement.complete) {
376
+ this.imageElement.addEventListener('load', this._updateScaling.bind(this));
377
+ }
378
+
379
+ // Écouteurs pour les interactions de souris
380
+ document.addEventListener('mouseup', this._handleMouseUp.bind(this));
381
+ document.addEventListener('mousemove', this._handleMouseMove.bind(this));
382
+ }
383
+
384
+ /**
385
+ * Gère l'événement mouseup global
386
+ * @private
387
+ */
388
+ _handleMouseUp() {
389
+ if (this.interaction.focusDragging) {
390
+ this.interaction.focusDragging = false;
391
+ if (this.state.focusMarker) this.state.focusMarker.style.cursor = 'move';
392
+ }
393
+
394
+ if (this.interaction.cropDragging) {
395
+ this.interaction.cropDragging = false;
396
+ if (this.state.cropOverlay) this.state.cropOverlay.style.cursor = 'move';
397
+ }
398
+
399
+ this.interaction.cropResizing = false;
400
+ this.interaction.activeHandle = null;
401
+ document.body.style.cursor = 'default';
402
+ }
403
+
404
+ /**
405
+ * Gère l'événement mousemove global
406
+ * @private
407
+ * @param {MouseEvent} e - Événement mousemove
408
+ */
409
+ _handleMouseMove(e) {
410
+ // Gestion du déplacement du point focal
411
+ if (this.interaction.focusDragging && this.state.focusMarker) {
412
+ this._handleFocusMarkerDrag(e);
413
+ }
414
+
415
+ // Gestion du déplacement de la zone de recadrage
416
+ if (this.interaction.cropDragging) {
417
+ this._handleCropOverlayDrag(e);
418
+ }
419
+
420
+ // Gestion du redimensionnement de la zone de recadrage
421
+ if (this.interaction.cropResizing) {
422
+ this._handleCropOverlayResize(e);
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Gère le déplacement du marqueur de point focal
428
+ * @private
429
+ * @param {MouseEvent} e - Événement mousemove
430
+ */
431
+ _handleFocusMarkerDrag(e) {
432
+ const imageRect = this.imageElement.getBoundingClientRect();
433
+
434
+ // Calculer la position cible relative à l'image
435
+ const targetScaledX = e.clientX - imageRect.left - this.interaction.focusDragOffsetX;
436
+ const targetScaledY = e.clientY - imageRect.top - this.interaction.focusDragOffsetY;
437
+
438
+ // Convertir en coordonnées originales
439
+ const original = this._toOriginalCoords(targetScaledX, targetScaledY);
440
+
441
+ // Mettre à jour le point focal
442
+ this.setFocusPoint(original.x, original.y);
443
+ }
444
+
445
+ /**
446
+ * Gère le déplacement de l'overlay de recadrage
447
+ * @private
448
+ * @param {MouseEvent} e - Événement mousemove
449
+ */
450
+ _handleCropOverlayDrag(e) {
451
+ const deltaX = e.clientX - this.interaction.startMouseX;
452
+ const deltaY = e.clientY - this.interaction.startMouseY;
453
+
454
+ // Calculer la nouvelle position en pixels d'affichage
455
+ let newX = this.interaction.startX + deltaX;
456
+ let newY = this.interaction.startY + deltaY;
457
+
458
+ // Convertir en coordonnées originales
459
+ const original = this._toOriginalCoords(newX, newY);
460
+
461
+ // Obtenir les dimensions actuelles en coordonnées originales
462
+ const { width, height } = this.state.cropZone;
463
+
464
+ // Mettre à jour la zone de recadrage
465
+ this.setCropZone(original.x, original.y, width, height);
466
+ }
467
+
468
+ /**
469
+ * Gère le redimensionnement de l'overlay de recadrage
470
+ * @private
471
+ * @param {MouseEvent} e - Événement mousemove
472
+ */
473
+ _handleCropOverlayResize(e) {
474
+ if (!this.interaction.activeHandle) return;
475
+
476
+ const deltaX = e.clientX - this.interaction.startMouseX;
477
+ const deltaY = e.clientY - this.interaction.startMouseY;
478
+
479
+ let newX = this.interaction.startX;
480
+ let newY = this.interaction.startY;
481
+ let newWidth = this.interaction.startWidth;
482
+ let newHeight = this.interaction.startHeight;
483
+
484
+ // Ajuster en fonction de la poignée active
485
+ if (this.interaction.activeHandle.includes('t')) { // Top
486
+ newY = this.interaction.startY + deltaY;
487
+ newHeight = this.interaction.startHeight - deltaY;
488
+ }
489
+ if (this.interaction.activeHandle.includes('b')) { // Bottom
490
+ newHeight = this.interaction.startHeight + deltaY;
491
+ }
492
+ if (this.interaction.activeHandle.includes('l')) { // Left
493
+ newX = this.interaction.startX + deltaX;
494
+ newWidth = this.interaction.startWidth - deltaX;
495
+ }
496
+ if (this.interaction.activeHandle.includes('r')) { // Right
497
+ newWidth = this.interaction.startWidth + deltaX;
498
+ }
499
+
500
+ // Empêcher les dimensions négatives
501
+ if (newWidth < 10) {
502
+ if (this.interaction.activeHandle.includes('l')) {
503
+ newX = this.interaction.startX + this.interaction.startWidth - 10;
504
+ }
505
+ newWidth = 10;
506
+ }
507
+ if (newHeight < 10) {
508
+ if (this.interaction.activeHandle.includes('t')) {
509
+ newY = this.interaction.startY + this.interaction.startHeight - 10;
510
+ }
511
+ newHeight = 10;
512
+ }
513
+
514
+ // Convertir les coordonnées d'affichage en coordonnées originales
515
+ const originalX = newX / this.state.scaleX;
516
+ const originalY = newY / this.state.scaleY;
517
+ const originalWidth = newWidth / this.state.scaleX;
518
+ const originalHeight = newHeight / this.state.scaleY;
519
+
520
+ // Mettre à jour la zone de recadrage
521
+ this.setCropZone(originalX, originalY, originalWidth, originalHeight);
522
+ }
523
+
524
+ /**
525
+ * Met à jour la position du marqueur de point focal
526
+ * @private
527
+ */
528
+ _updateFocusMarkerPosition() {
529
+ if (!this.state.focusMarker) return;
530
+
531
+ const { x, y } = this.state.focusPoint;
532
+
533
+ // Limiter les coordonnées aux dimensions de l'image
534
+ const clampedX = Math.max(0, Math.min(x, this.state.originalWidth));
535
+ const clampedY = Math.max(0, Math.min(y, this.state.originalHeight));
536
+
537
+ // Mettre à jour l'état si les coordonnées ont été limitées
538
+ if (x !== clampedX || y !== clampedY) {
539
+ this.state.focusPoint = { x: clampedX, y: clampedY };
540
+ }
541
+
542
+ const scaled = this._toScaledCoords(clampedX, clampedY);
543
+
544
+ // Ajuster pour centrer le marqueur
545
+ this.state.focusMarker.style.left = (scaled.x - this.state.focusMarker.offsetWidth / 2) + 'px';
546
+ this.state.focusMarker.style.top = (scaled.y - this.state.focusMarker.offsetHeight / 2) + 'px';
547
+ }
548
+
549
+ /**
550
+ * Met à jour la position et les dimensions de l'overlay de recadrage
551
+ * @private
552
+ */
553
+ _updateCropOverlayPosition() {
554
+ if (!this.state.cropOverlay) return;
555
+
556
+ const { x, y, width, height } = this.state.cropZone;
557
+
558
+ // Limiter aux dimensions de l'image
559
+ const clampedX = Math.max(0, Math.min(x, this.state.originalWidth - width));
560
+ const clampedY = Math.max(0, Math.min(y, this.state.originalHeight - height));
561
+ const clampedWidth = Math.max(10, Math.min(width, this.state.originalWidth - clampedX));
562
+ const clampedHeight = Math.max(10, Math.min(height, this.state.originalHeight - clampedY));
563
+
564
+ // Mettre à jour l'état si les valeurs ont été limitées
565
+ if (x !== clampedX || y !== clampedY || width !== clampedWidth || height !== clampedHeight) {
566
+ this.state.cropZone = { x: clampedX, y: clampedY, width: clampedWidth, height: clampedHeight };
567
+ }
568
+
569
+ // Convertir en coordonnées d'affichage
570
+ const scaled = this._toScaledCoords(clampedX, clampedY);
571
+ const scaledWidth = clampedWidth * this.state.scaleX;
572
+ const scaledHeight = clampedHeight * this.state.scaleY;
573
+
574
+ // Mettre à jour l'overlay
575
+ this.state.cropOverlay.style.left = scaled.x + 'px';
576
+ this.state.cropOverlay.style.top = scaled.y + 'px';
577
+ this.state.cropOverlay.style.width = scaledWidth + 'px';
578
+ this.state.cropOverlay.style.height = scaledHeight + 'px';
579
+ }
580
+
581
+ /**
582
+ * Active ou désactive le point focal
583
+ * @public
584
+ * @param {boolean} active - État d'activation
585
+ * @returns {ImageTool} Instance pour chaînage
586
+ */
587
+ toggleFocusPoint(active) {
588
+ if (!this.options.focusPoint.enabled) return this;
589
+
590
+ if (active === undefined) {
591
+ active = !this.state.focusActive;
592
+ }
593
+
594
+ if (active && !this.state.focusActive) {
595
+ // Activer le point focal
596
+ if (!this.state.focusMarker) {
597
+ this._createFocusMarker();
598
+ }
599
+
600
+ // Positionner au centre de l'image par défaut si pas déjà défini
601
+ if (this.state.focusPoint.x === 0 && this.state.focusPoint.y === 0) {
602
+ this.state.focusPoint = {
603
+ x: this.state.originalWidth / 2,
604
+ y: this.state.originalHeight / 2
605
+ };
606
+ }
607
+
608
+ this._updateFocusMarkerPosition();
609
+ this.state.focusMarker.style.display = 'block';
610
+ this.state.focusActive = true;
611
+ } else if (!active && this.state.focusActive) {
612
+ // Désactiver le point focal
613
+ if (this.state.focusMarker) {
614
+ this.state.focusMarker.style.display = 'none';
615
+ }
616
+ this.state.focusActive = false;
617
+ }
618
+
619
+ // Notifier le changement
620
+ this._notifyChange();
621
+
622
+ return this;
623
+ }
624
+
625
+ /**
626
+ * Active ou désactive la zone de recadrage
627
+ * @public
628
+ * @param {boolean} active - État d'activation
629
+ * @returns {ImageTool} Instance pour chaînage
630
+ */
631
+ toggleCropZone(active) {
632
+ if (!this.options.cropZone.enabled) return this;
633
+
634
+ if (active === undefined) {
635
+ active = !this.state.cropActive;
636
+ }
637
+
638
+ if (active && !this.state.cropActive) {
639
+ // Activer la zone de recadrage
640
+ if (!this.state.cropOverlay) {
641
+ this._createCropOverlay();
642
+ }
643
+
644
+ // Définir une zone par défaut si pas déjà définie
645
+ if (this.state.cropZone.width === 0 || this.state.cropZone.height === 0) {
646
+ const defaultWidth = this.state.originalWidth / 2;
647
+ const defaultHeight = this.state.originalHeight / 2;
648
+ const defaultX = (this.state.originalWidth - defaultWidth) / 2;
649
+ const defaultY = (this.state.originalHeight - defaultHeight) / 2;
650
+
651
+ this.state.cropZone = {
652
+ x: defaultX,
653
+ y: defaultY,
654
+ width: defaultWidth,
655
+ height: defaultHeight
656
+ };
657
+ }
658
+
659
+ this._updateCropOverlayPosition();
660
+ this.state.cropOverlay.style.display = 'block';
661
+ this.state.cropActive = true;
662
+ } else if (!active && this.state.cropActive) {
663
+ // Désactiver la zone de recadrage
664
+ if (this.state.cropOverlay) {
665
+ this.state.cropOverlay.style.display = 'none';
666
+ }
667
+ this.state.cropActive = false;
668
+ }
669
+
670
+ // Notifier le changement
671
+ this._notifyChange();
672
+
673
+ return this;
674
+ }
675
+
676
+ /**
677
+ * Définit la position du point focal
678
+ * @public
679
+ * @param {number} x - Coordonnée X en pixels originaux
680
+ * @param {number} y - Coordonnée Y en pixels originaux
681
+ * @returns {ImageTool} Instance pour chaînage
682
+ */
683
+ setFocusPoint(x, y) {
684
+ // Limiter les coordonnées aux dimensions de l'image
685
+ const clampedX = Math.max(0, Math.min(x, this.state.originalWidth));
686
+ const clampedY = Math.max(0, Math.min(y, this.state.originalHeight));
687
+
688
+ this.state.focusPoint = { x: clampedX, y: clampedY };
689
+
690
+ if (this.state.focusActive && this.state.focusMarker) {
691
+ this._updateFocusMarkerPosition();
692
+ }
693
+
694
+ // Notifier le changement
695
+ this._notifyChange();
696
+
697
+ return this;
698
+ }
699
+
700
+ /**
701
+ * Définit la position et les dimensions de la zone de recadrage
702
+ * @public
703
+ * @param {number} x - Coordonnée X en pixels originaux
704
+ * @param {number} y - Coordonnée Y en pixels originaux
705
+ * @param {number} width - Largeur en pixels originaux
706
+ * @param {number} height - Hauteur en pixels originaux
707
+ * @returns {ImageTool} Instance pour chaînage
708
+ */
709
+ setCropZone(x, y, width, height) {
710
+ // Limiter aux dimensions de l'image
711
+ const clampedX = Math.max(0, Math.min(x, this.state.originalWidth - width));
712
+ const clampedY = Math.max(0, Math.min(y, this.state.originalHeight - height));
713
+ const clampedWidth = Math.max(10, Math.min(width, this.state.originalWidth - clampedX));
714
+ const clampedHeight = Math.max(10, Math.min(height, this.state.originalHeight - clampedY));
715
+
716
+ this.state.cropZone = {
717
+ x: clampedX,
718
+ y: clampedY,
719
+ width: clampedWidth,
720
+ height: clampedHeight
721
+ };
722
+
723
+ if (this.state.cropActive && this.state.cropOverlay) {
724
+ this._updateCropOverlayPosition();
725
+ }
726
+
727
+ // Notifier le changement
728
+ this._notifyChange();
729
+
730
+ return this;
731
+ }
732
+
733
+ /**
734
+ * Obtient la position actuelle du point focal
735
+ * @public
736
+ * @returns {Object} Coordonnées du point focal {x, y}
737
+ */
738
+ getFocusPoint() {
739
+ return { ...this.state.focusPoint };
740
+ }
741
+
742
+ /**
743
+ * Obtient la position et les dimensions actuelles de la zone de recadrage
744
+ * @public
745
+ * @returns {Object} Zone de recadrage {x, y, width, height}
746
+ */
747
+ getCropZone() {
748
+ return { ...this.state.cropZone };
749
+ }
750
+
751
+ /**
752
+ * Obtient les dimensions originales de l'image
753
+ * @public
754
+ * @returns {Object} Dimensions {width, height}
755
+ */
756
+ getImageDimensions() {
757
+ return {
758
+ width: this.state.originalWidth,
759
+ height: this.state.originalHeight
760
+ };
761
+ }
762
+
763
+ /**
764
+ * Notifie les changements via le callback
765
+ * @private
766
+ */
767
+ _notifyChange() {
768
+ if (typeof this.options.onChange === 'function') {
769
+ this.options.onChange({
770
+ focusPoint: this.getFocusPoint(),
771
+ cropZone: this.getCropZone(),
772
+ focusActive: this.state.focusActive,
773
+ cropActive: this.state.cropActive
774
+ });
775
+ }
776
+ }
777
+
778
+ /**
779
+ * Détruit l'instance et nettoie les ressources
780
+ * @public
781
+ */
782
+ destroy() {
783
+ // Supprimer les éléments DOM
784
+ if (this.state.focusMarker && this.state.focusMarker.parentNode) {
785
+ this.state.focusMarker.parentNode.removeChild(this.state.focusMarker);
786
+ }
787
+
788
+ if (this.state.cropOverlay && this.state.cropOverlay.parentNode) {
789
+ this.state.cropOverlay.parentNode.removeChild(this.state.cropOverlay);
790
+ }
791
+
792
+ // Supprimer les écouteurs d'événements
793
+ window.removeEventListener('resize', this._updateScaling.bind(this));
794
+ document.removeEventListener('mouseup', this._handleMouseUp.bind(this));
795
+ document.removeEventListener('mousemove', this._handleMouseMove.bind(this));
796
+
797
+ // Réinitialiser l'état
798
+ this.state = null;
799
+ this.interaction = null;
800
+ this.options = null;
801
+ this.imageElement = null;
802
+ }
803
+ }
804
+
805
+ /**
806
+ * Point d'entrée principal pour le package image-tool
807
+ * Exporte la classe VisualImageTool
808
+ */
809
+
810
+ exports.VisualImageTool = VisualImageTool;
811
+ exports["default"] = VisualImageTool;
812
+ //# sourceMappingURL=visual-image-tool.js.map