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