canvasframework 0.5.60 → 0.5.61

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.
@@ -280,35 +280,34 @@ class Camera extends Component {
280
280
  }
281
281
 
282
282
  handlePress(relX, relY) {
283
-
284
- // Capture centrale
285
- const captureX = this.width / 2;
286
- const captureY = this.height - 60;
287
- if (Math.hypot(relX - captureX, relY - captureY) < this.captureButtonRadius + 10) {
288
- this.capturePhoto();
289
- return;
290
- }
291
-
292
- // Switch caméra
293
- if (relX < 60 && relY < 60) {
294
- this.switchCamera();
295
- return;
296
- }
297
-
298
- // Torch
299
- if (this.torchSupported && relX > this.width - 60 && relY < 60) {
300
- this.toggleTorch();
301
- return;
302
- }
303
-
304
- // Switch mode
305
- const modeButtonX = this.width - 80;
306
- const modeButtonY = 20;
307
- if (relX > modeButtonX && relX < modeButtonX + this.modeButtonSize &&
308
- relY > modeButtonY && relY < modeButtonY + this.modeButtonSize) {
309
- this.switchFitMode();
310
- return;
311
- }
283
+ // Capture centrale
284
+ const captureX = this.width / 2;
285
+ const captureY = this.height - 60;
286
+ if (Math.hypot(relX - captureX, relY - captureY) < this.captureButtonRadius + 10) {
287
+ this.capturePhoto();
288
+ return;
289
+ }
290
+
291
+ // Switch caméra (haut gauche)
292
+ if (relX < 70 && relY < 70) {
293
+ this.switchCamera();
294
+ return;
295
+ }
296
+
297
+ // Torch (haut droite) - zone précise
298
+ if (this.torchSupported && relX > this.width - 75 && relX < this.width - 25 && relY > 20 && relY < 70) {
299
+ this.toggleTorch();
300
+ return;
301
+ }
302
+
303
+ // Switch mode (haut droite, décalé à gauche de la torche)
304
+ const modeButtonX = this.width - 130;
305
+ const modeButtonY = 20;
306
+ if (relX > modeButtonX && relX < modeButtonX + this.modeButtonSize &&
307
+ relY > modeButtonY && relY < modeButtonY + this.modeButtonSize) {
308
+ this.switchFitMode();
309
+ return;
310
+ }
312
311
  }
313
312
 
314
313
  drawContainIcon(ctx, x, y, size) {
@@ -524,132 +523,139 @@ class Camera extends Component {
524
523
  }
525
524
 
526
525
  draw(ctx) {
527
- ctx.save();
526
+ ctx.save();
528
527
 
529
- ctx.fillStyle = '#000';
530
- ctx.fillRect(this.x, this.y, this.width, this.height);
528
+ ctx.fillStyle = '#000';
529
+ ctx.fillRect(this.x, this.y, this.width, this.height);
531
530
 
532
- // Flash blanc après capture
533
- if (this.flashTimer) {
534
- ctx.fillStyle = 'rgba(255,255,255,0.6)';
535
- ctx.fillRect(this.x, this.y, this.width, this.height);
536
- }
531
+ // Flash blanc après capture
532
+ if (this.flashTimer) {
533
+ ctx.fillStyle = 'rgba(255,255,255,0.6)';
534
+ ctx.fillRect(this.x, this.y, this.width, this.height);
535
+ }
537
536
 
538
- if (this.error) {
539
- ctx.fillStyle = '#ff4444';
540
- ctx.font = '16px Arial';
541
- ctx.textAlign = 'center';
542
- ctx.textBaseline = 'middle';
543
- ctx.fillText(this.error, this.x + this.width/2, this.y + this.height/2);
544
- } else if (!this.loaded) {
545
- ctx.fillStyle = '#fff';
546
- ctx.font = '16px Arial';
547
- ctx.textAlign = 'center';
548
- ctx.textBaseline = 'middle';
549
- ctx.fillText('Démarrage caméra...', this.x + this.width/2, this.y + this.height/2);
550
- } else if (this.video && this.loaded) {
551
- const videoRatio = this.video.videoWidth / this.video.videoHeight;
552
- const canvasRatio = this.width / this.height;
553
-
554
- let drawWidth = this.width;
555
- let drawHeight = this.height;
556
- let offsetX = 0;
557
- let offsetY = 0;
558
-
559
- if (this.fitMode === 'cover') {
560
- if (videoRatio > canvasRatio) {
561
- drawHeight = this.height;
562
- drawWidth = drawHeight * videoRatio;
563
- offsetX = (this.width - drawWidth) / 2;
564
- } else {
565
- drawWidth = this.width;
566
- drawHeight = drawWidth / videoRatio;
567
- offsetY = (this.height - drawHeight) / 2;
568
- }
569
- } else if (this.fitMode === 'contain') {
570
- if (videoRatio > canvasRatio) {
571
- drawWidth = this.width;
572
- drawHeight = drawWidth / videoRatio;
573
- offsetY = (this.height - drawHeight) / 2;
574
- } else {
575
- drawHeight = this.height;
576
- drawWidth = drawHeight * videoRatio;
577
- offsetX = (this.width - drawWidth) / 2;
578
- }
579
- }
580
-
581
- ctx.drawImage(this.video, this.x + offsetX, this.y + offsetY, drawWidth, drawHeight);
582
-
583
- // Mini preview dernière photo (bas droite, 3s)
584
- if (this.previewPhoto) {
585
- const previewSize = 80;
586
- const img = new Image();
587
- img.src = this.previewPhoto;
588
- ctx.drawImage(img, this.x + this.width - previewSize - 10, this.y + this.height - previewSize - 10, previewSize, previewSize);
589
- ctx.strokeStyle = '#fff';
590
- ctx.lineWidth = 2;
591
- ctx.strokeRect(this.x + this.width - previewSize - 10, this.y + this.height - previewSize - 10, previewSize, previewSize);
537
+ if (this.error) {
538
+ ctx.fillStyle = '#ff4444';
539
+ ctx.font = '16px Arial';
540
+ ctx.textAlign = 'center';
541
+ ctx.textBaseline = 'middle';
542
+ ctx.fillText(this.error, this.x + this.width/2, this.y + this.height/2);
543
+ } else if (!this.loaded) {
544
+ ctx.fillStyle = '#fff';
545
+ ctx.font = '16px Arial';
546
+ ctx.textAlign = 'center';
547
+ ctx.textBaseline = 'middle';
548
+ ctx.fillText('Démarrage caméra...', this.x + this.width/2, this.y + this.height/2);
549
+ } else if (this.video && this.loaded) {
550
+ const videoRatio = this.video.videoWidth / this.video.videoHeight;
551
+ const canvasRatio = this.width / this.height;
552
+
553
+ let drawWidth = this.width;
554
+ let drawHeight = this.height;
555
+ let offsetX = 0;
556
+ let offsetY = 0;
557
+
558
+ if (this.fitMode === 'cover') {
559
+ if (videoRatio > canvasRatio) {
560
+ drawHeight = this.height;
561
+ drawWidth = drawHeight * videoRatio;
562
+ offsetX = (this.width - drawWidth) / 2;
563
+ } else {
564
+ drawWidth = this.width;
565
+ drawHeight = drawWidth / videoRatio;
566
+ offsetY = (this.height - drawHeight) / 2;
567
+ }
568
+ } else if (this.fitMode === 'contain') {
569
+ if (videoRatio > canvasRatio) {
570
+ drawWidth = this.width;
571
+ drawHeight = drawWidth / videoRatio;
572
+ offsetY = (this.height - drawHeight) / 2;
573
+ } else {
574
+ drawHeight = this.height;
575
+ drawWidth = drawHeight * videoRatio;
576
+ offsetX = (this.width - drawWidth) / 2;
592
577
  }
593
578
  }
594
579
 
595
- // Contrôles bas
596
- ctx.fillStyle = 'rgba(0,0,0,0.5)';
597
- ctx.fillRect(this.x, this.y + this.height - 100, this.width, 100);
580
+ ctx.drawImage(this.video, this.x + offsetX, this.y + offsetY, drawWidth, drawHeight);
581
+
582
+ // Mini preview dernière photo (bas droite, 3s)
583
+ if (this.previewPhoto) {
584
+ const previewSize = 80;
585
+ const img = new Image();
586
+ img.src = this.previewPhoto;
587
+ ctx.drawImage(img, this.x + this.width - previewSize - 10, this.y + this.height - previewSize - 10, previewSize, previewSize);
588
+ ctx.strokeStyle = '#fff';
589
+ ctx.lineWidth = 2;
590
+ ctx.strokeRect(this.x + this.width - previewSize - 10, this.y + this.height - previewSize - 10, previewSize, previewSize);
591
+ }
592
+ }
598
593
 
599
- // Bouton capture
600
- ctx.fillStyle = '#ffffff';
594
+ // Contrôles bas
595
+ ctx.fillStyle = 'rgba(0,0,0,0.5)';
596
+ ctx.fillRect(this.x, this.y + this.height - 100, this.width, 100);
597
+
598
+ // Bouton capture
599
+ ctx.fillStyle = '#ffffff';
600
+ ctx.beginPath();
601
+ ctx.arc(this.x + this.width/2, this.y + this.height - 50, this.captureButtonRadius, 0, Math.PI * 2);
602
+ ctx.fill();
603
+
604
+ ctx.strokeStyle = '#ff4444';
605
+ ctx.lineWidth = 6;
606
+ ctx.beginPath();
607
+ ctx.arc(this.x + this.width/2, this.y + this.height - 50, this.captureButtonRadius + 10, 0, Math.PI * 2);
608
+ ctx.stroke();
609
+
610
+ // Switch caméra avec icône
611
+ const switchBtnX = this.x + 20;
612
+ const switchBtnY = this.y + 20;
613
+ const switchBtnSize = 50;
614
+
615
+ ctx.fillStyle = 'rgba(0,0,0,0.5)';
616
+ ctx.beginPath();
617
+ ctx.arc(switchBtnX + switchBtnSize/2, switchBtnY + switchBtnSize/2, switchBtnSize/2, 0, Math.PI * 2);
618
+ ctx.fill();
619
+
620
+ this.drawSwitchCameraIcon(ctx, switchBtnX, switchBtnY, switchBtnSize);
621
+
622
+ // Bouton torche (haut droite)
623
+ if (this.torchSupported) {
624
+ const torchBtnX = this.x + this.width - 75;
625
+ const torchBtnY = this.y + 20;
626
+ const torchBtnSize = 50;
627
+
628
+ ctx.fillStyle = this.torchOn ? 'rgba(255,235,59,0.8)' : 'rgba(0,0,0,0.5)';
601
629
  ctx.beginPath();
602
- ctx.arc(this.x + this.width/2, this.y + this.height - 50, this.captureButtonRadius, 0, Math.PI * 2);
630
+ ctx.arc(torchBtnX + torchBtnSize/2, torchBtnY + torchBtnSize/2, torchBtnSize/2, 0, Math.PI * 2);
603
631
  ctx.fill();
604
632
 
605
- ctx.strokeStyle = '#ff4444';
606
- ctx.lineWidth = 6;
607
- ctx.beginPath();
608
- ctx.arc(this.x + this.width/2, this.y + this.height - 50, this.captureButtonRadius + 10, 0, Math.PI * 2);
609
- ctx.stroke();
633
+ ctx.fillStyle = '#fff';
634
+ ctx.font = '24px Arial';
635
+ ctx.textAlign = 'center';
636
+ ctx.textBaseline = 'middle';
637
+ ctx.fillText('⚡', torchBtnX + torchBtnSize/2, torchBtnY + torchBtnSize/2);
638
+ }
610
639
 
611
- // Switch caméra avec icône
612
- const switchBtnX = this.x + 20;
613
- const switchBtnY = this.y + 20;
614
- const switchBtnSize = 50;
615
-
616
- // Fond semi-transparent
617
- ctx.fillStyle = 'rgba(0,0,0,0.5)';
618
- ctx.beginPath();
619
- ctx.arc(switchBtnX + switchBtnSize/2, switchBtnY + switchBtnSize/2, switchBtnSize/2, 0, Math.PI * 2);
620
- ctx.fill();
621
-
622
- // Icône
623
- this.drawSwitchCameraIcon(ctx, switchBtnX, switchBtnY, switchBtnSize);
640
+ // Bouton switch mode (décalé à gauche de la torche)
641
+ const btnX = this.x + this.width - 130;
642
+ const btnY = this.y + 20;
624
643
 
625
- // Torch
626
- if (this.torchSupported) {
627
- ctx.fillStyle = this.torchOn ? '#ffeb3b' : '#ffffff';
628
- ctx.fillText('⚡', this.x + this.width - 50, this.y + 45);
629
- }
644
+ ctx.fillStyle = 'rgba(255,255,255,0.9)';
645
+ ctx.fillRect(btnX, btnY, this.modeButtonSize, this.modeButtonSize);
630
646
 
631
- // Bouton switch mode avec icône
632
- const btnX = this.x + this.width - 80;
633
- const btnY = this.y + 20;
634
-
635
- // Fond du bouton
636
- ctx.fillStyle = 'rgba(255,255,255,0.9)';
637
- ctx.fillRect(btnX, btnY, this.modeButtonSize, this.modeButtonSize);
638
-
639
- // Bordure
640
- ctx.strokeStyle = '#000';
641
- ctx.lineWidth = 2;
642
- ctx.strokeRect(btnX, btnY, this.modeButtonSize, this.modeButtonSize);
643
-
644
- // Dessiner l'icône appropriée
645
- if (this.fitMode === 'contain') {
646
- this.drawContainIcon(ctx, btnX, btnY, this.modeButtonSize);
647
- } else {
648
- this.drawCoverIcon(ctx, btnX, btnY, this.modeButtonSize);
649
- }
647
+ ctx.strokeStyle = '#000';
648
+ ctx.lineWidth = 2;
649
+ ctx.strokeRect(btnX, btnY, this.modeButtonSize, this.modeButtonSize);
650
650
 
651
- ctx.restore();
651
+ if (this.fitMode === 'contain') {
652
+ this.drawContainIcon(ctx, btnX, btnY, this.modeButtonSize);
653
+ } else {
654
+ this.drawCoverIcon(ctx, btnX, btnY, this.modeButtonSize);
652
655
  }
656
+
657
+ ctx.restore();
658
+ }
653
659
  }
654
660
 
655
661
  export default Camera;
@@ -2,7 +2,8 @@ import Component from '../core/Component.js';
2
2
 
3
3
  /**
4
4
  * Composant Camera autonome avec gestion directe des clics/touches
5
- * Modes : contain (tout visible + bandes), cover (remplit + crop), fit (centre sans crop)
5
+ * Modes : contain (tout visible + bandes), cover (remplit + crop)
6
+ * Layout boutons haut : [switch caméra GAUCHE] [torch CENTRE] [mode DROITE]
6
7
  */
7
8
  class FloatedCamera extends Component {
8
9
  constructor(framework, options = {}) {
@@ -29,27 +30,28 @@ class FloatedCamera extends Component {
29
30
 
30
31
  // Feedback capture
31
32
  this.flashTimer = null;
32
- this.previewPhoto = null; // dataUrl dernière photo
33
+ this.previewPhoto = null;
33
34
  this.previewTimeout = null;
34
35
 
35
36
  this.isStarting = false;
37
+
38
+ // Taille et position des boutons (haut)
39
+ this.topBtnSize = 50;
40
+ this.topBtnMargin = 20;
36
41
  }
37
-
42
+
38
43
  static cleanupAllCameras() {
39
44
  const allVideos = document.querySelectorAll('video');
40
45
  allVideos.forEach(video => {
41
- // Arrêter les streams
42
46
  if (video.srcObject) {
43
- const stream = video.srcObject;
44
- if (stream && stream.getTracks) {
45
- stream.getTracks().forEach(track => track.stop());
46
- }
47
- video.srcObject = null;
48
- }
49
-
50
- // Supprimer du DOM
47
+ const stream = video.srcObject;
48
+ if (stream && stream.getTracks) {
49
+ stream.getTracks().forEach(track => track.stop());
50
+ }
51
+ video.srcObject = null;
52
+ }
51
53
  if (video.parentNode) {
52
- video.parentNode.removeChild(video);
54
+ video.parentNode.removeChild(video);
53
55
  }
54
56
  });
55
57
  }
@@ -57,7 +59,6 @@ class FloatedCamera extends Component {
57
59
  async _mount() {
58
60
  super._mount?.();
59
61
 
60
- // ✅ CORRECTION : Ne démarrer que si visible ET pas en navigation
61
62
  if (this.visible && this.autoStart && !this.stream && !this.isStarting && !this.framework._isNavigating) {
62
63
  this.isStarting = true;
63
64
  await this.startCamera();
@@ -66,7 +67,7 @@ class FloatedCamera extends Component {
66
67
 
67
68
  this.setupEventListeners();
68
69
  }
69
-
70
+
70
71
  onUnmount() {
71
72
  this.removeEventListeners();
72
73
  this.stopCamera();
@@ -82,7 +83,6 @@ class FloatedCamera extends Component {
82
83
  super.destroy?.();
83
84
  }
84
85
 
85
- // Écoute directe (indépendante du framework)
86
86
  setupEventListeners() {
87
87
  this.onTouchStart = this.handleTouchStart.bind(this);
88
88
  this.onTouchMove = this.handleTouchMove.bind(this);
@@ -110,7 +110,6 @@ class FloatedCamera extends Component {
110
110
  canvas.removeEventListener('mouseup', this.onMouseUp);
111
111
  }
112
112
 
113
- // Coordonnées locales
114
113
  getLocalPos(clientX, clientY) {
115
114
  const rect = this.framework.canvas.getBoundingClientRect();
116
115
  const globalX = clientX - rect.left;
@@ -128,13 +127,8 @@ class FloatedCamera extends Component {
128
127
  this.handlePress(pos.x, pos.y);
129
128
  }
130
129
 
131
- handleTouchMove(e) {
132
- e.preventDefault();
133
- }
134
-
135
- handleTouchEnd(e) {
136
- e.preventDefault();
137
- }
130
+ handleTouchMove(e) { e.preventDefault(); }
131
+ handleTouchEnd(e) { e.preventDefault(); }
138
132
 
139
133
  handleMouseDown(e) {
140
134
  const pos = this.getLocalPos(e.clientX, e.clientY);
@@ -142,11 +136,10 @@ class FloatedCamera extends Component {
142
136
  }
143
137
 
144
138
  handleMouseMove(e) {}
145
-
146
139
  handleMouseUp(e) {}
147
140
 
148
141
  async startCamera() {
149
- FloatedCamera.cleanupAllCameras();
142
+ FloatedCamera.cleanupAllCameras();
150
143
  if (this.stream) return;
151
144
 
152
145
  try {
@@ -253,7 +246,6 @@ class FloatedCamera extends Component {
253
246
  if (this.onPhoto) this.onPhoto(dataUrl);
254
247
  console.log('Photo capturée ! DataURL:', dataUrl.substring(0, 50) + '...');
255
248
 
256
- // Feedback : flash + preview 3s
257
249
  this.flashTimer = setTimeout(() => {
258
250
  this.flashTimer = null;
259
251
  this.markDirty();
@@ -275,257 +267,216 @@ class FloatedCamera extends Component {
275
267
  this.markDirty();
276
268
  }
277
269
 
270
+ // ─── Zones des boutons ───────────────────────────────────────────────────────
271
+ // Layout haut : [switch caméra GAUCHE] [torch CENTRE] [mode DROITE]
272
+
273
+ _getSwitchBtnBounds() {
274
+ return {
275
+ x: this.topBtnMargin,
276
+ y: this.topBtnMargin,
277
+ size: this.topBtnSize
278
+ };
279
+ }
280
+
281
+ _getTorchBtnBounds() {
282
+ // Centré horizontalement dans le composant
283
+ return {
284
+ x: (this.width - this.topBtnSize) / 2,
285
+ y: this.topBtnMargin,
286
+ size: this.topBtnSize
287
+ };
288
+ }
289
+
290
+ _getModeBtnBounds() {
291
+ // Droite, centré verticalement par rapport aux autres boutons
292
+ const yCenter = this.topBtnMargin + this.topBtnSize / 2;
293
+ return {
294
+ x: this.width - this.topBtnMargin - this.modeButtonSize,
295
+ y: yCenter - this.modeButtonSize / 2,
296
+ size: this.modeButtonSize
297
+ };
298
+ }
299
+
300
+ _getCaptureBtnCenter() {
301
+ return {
302
+ cx: this.width / 2,
303
+ cy: this.height - 50
304
+ };
305
+ }
306
+
307
+ // ─── Gestion des clics ──────────────────────────────────────────────────────
308
+
278
309
  handlePress(relX, relY) {
310
+ // Guard : ignorer tout clic hors du composant
311
+ if (relX < 0 || relX > this.width || relY < 0 || relY > this.height) return;
279
312
 
280
- // Capture centrale
281
- const captureX = this.width / 2;
282
- const captureY = this.height - 60;
283
- if (Math.hypot(relX - captureX, relY - captureY) < this.captureButtonRadius + 10) {
313
+ // Bouton capture
314
+ const { cx, cy } = this._getCaptureBtnCenter();
315
+ if (Math.hypot(relX - cx, relY - cy) < this.captureButtonRadius + 10) {
284
316
  this.capturePhoto();
285
317
  return;
286
318
  }
287
319
 
288
- // Switch caméra
289
- if (relX < 60 && relY < 60) {
320
+ // Switch caméra (GAUCHE)
321
+ const sw = this._getSwitchBtnBounds();
322
+ if (Math.hypot(relX - (sw.x + sw.size / 2), relY - (sw.y + sw.size / 2)) < sw.size / 2 + 5) {
290
323
  this.switchCamera();
291
324
  return;
292
325
  }
293
326
 
294
- // Torch
295
- if (this.torchSupported && relX > this.width - 60 && relY < 60) {
296
- this.toggleTorch();
297
- return;
327
+ // Torch (CENTRE)
328
+ if (this.torchSupported) {
329
+ const tb = this._getTorchBtnBounds();
330
+ if (Math.hypot(relX - (tb.x + tb.size / 2), relY - (tb.y + tb.size / 2)) < tb.size / 2 + 5) {
331
+ this.toggleTorch();
332
+ return;
333
+ }
298
334
  }
299
335
 
300
- // Switch mode
301
- const modeButtonX = this.width - 80;
302
- const modeButtonY = 20;
303
- if (relX > modeButtonX && relX < modeButtonX + this.modeButtonSize &&
304
- relY > modeButtonY && relY < modeButtonY + this.modeButtonSize) {
336
+ // Mode contain/cover (DROITE)
337
+ const mb = this._getModeBtnBounds();
338
+ if (relX >= mb.x && relX <= mb.x + mb.size && relY >= mb.y && relY <= mb.y + mb.size) {
305
339
  this.switchFitMode();
306
340
  return;
307
341
  }
308
342
  }
309
343
 
344
+ // ─── Icônes ─────────────────────────────────────────────────────────────────
345
+
310
346
  drawContainIcon(ctx, x, y, size) {
311
- // Icône "contain" : rectangle avec flèches vers l'intérieur
312
347
  const pad = size * 0.2;
313
348
  ctx.strokeStyle = '#000';
314
349
  ctx.lineWidth = 2;
315
-
316
- // Rectangle extérieur
317
350
  ctx.strokeRect(x + pad, y + pad, size - pad * 2, size - pad * 2);
318
-
319
- // Flèches pointant vers l'intérieur
320
- const arrowSize = size * 0.15;
321
- ctx.fillStyle = '#000';
322
-
323
- // Flèche haut
324
- ctx.beginPath();
325
- ctx.moveTo(x + size/2, y + pad - 2);
326
- ctx.lineTo(x + size/2 - arrowSize, y + pad + arrowSize);
327
- ctx.lineTo(x + size/2 + arrowSize, y + pad + arrowSize);
328
- ctx.fill();
329
-
330
- // Flèche bas
331
- ctx.beginPath();
332
- ctx.moveTo(x + size/2, y + size - pad + 2);
333
- ctx.lineTo(x + size/2 - arrowSize, y + size - pad - arrowSize);
334
- ctx.lineTo(x + size/2 + arrowSize, y + size - pad - arrowSize);
335
- ctx.fill();
336
-
337
- // Flèche gauche
338
- ctx.beginPath();
339
- ctx.moveTo(x + pad - 2, y + size/2);
340
- ctx.lineTo(x + pad + arrowSize, y + size/2 - arrowSize);
341
- ctx.lineTo(x + pad + arrowSize, y + size/2 + arrowSize);
342
- ctx.fill();
343
-
344
- // Flèche droite
345
- ctx.beginPath();
346
- ctx.moveTo(x + size - pad + 2, y + size/2);
347
- ctx.lineTo(x + size - pad - arrowSize, y + size/2 - arrowSize);
348
- ctx.lineTo(x + size - pad - arrowSize, y + size/2 + arrowSize);
349
- ctx.fill();
350
- }
351
351
 
352
- drawCoverIcon(ctx, x, y, size) {
353
- // Icône "cover" : rectangle avec flèches vers l'extérieur
354
- const pad = size * 0.2;
355
- ctx.strokeStyle = '#000';
356
- ctx.lineWidth = 2;
357
-
358
- // Rectangle intérieur
359
- ctx.strokeRect(x + pad, y + pad, size - pad * 2, size - pad * 2);
360
-
361
- // Flèches pointant vers l'extérieur
362
352
  const arrowSize = size * 0.15;
363
353
  ctx.fillStyle = '#000';
364
-
365
- // Flèche haut
366
- ctx.beginPath();
367
- ctx.moveTo(x + size/2, y + 2);
368
- ctx.lineTo(x + size/2 - arrowSize, y + arrowSize + 2);
369
- ctx.lineTo(x + size/2 + arrowSize, y + arrowSize + 2);
370
- ctx.fill();
371
-
372
- // Flèche bas
373
- ctx.beginPath();
374
- ctx.moveTo(x + size/2, y + size - 2);
375
- ctx.lineTo(x + size/2 - arrowSize, y + size - arrowSize - 2);
376
- ctx.lineTo(x + size/2 + arrowSize, y + size - arrowSize - 2);
377
- ctx.fill();
378
-
379
- // Flèche gauche
380
- ctx.beginPath();
381
- ctx.moveTo(x + 2, y + size/2);
382
- ctx.lineTo(x + arrowSize + 2, y + size/2 - arrowSize);
383
- ctx.lineTo(x + arrowSize + 2, y + size/2 + arrowSize);
384
- ctx.fill();
385
-
386
- // Flèche droite
387
- ctx.beginPath();
388
- ctx.moveTo(x + size - 2, y + size/2);
389
- ctx.lineTo(x + size - arrowSize - 2, y + size/2 - arrowSize);
390
- ctx.lineTo(x + size - arrowSize - 2, y + size/2 + arrowSize);
391
- ctx.fill();
392
- }
393
354
 
394
- drawContainIcon(ctx, x, y, size) {
395
- // Icône "contain" : rectangle avec flèches vers l'intérieur
396
- const pad = size * 0.2;
397
- ctx.strokeStyle = '#000';
398
- ctx.lineWidth = 2;
399
-
400
- // Rectangle extérieur
401
- ctx.strokeRect(x + pad, y + pad, size - pad * 2, size - pad * 2);
402
-
403
- // Flèches pointant vers l'intérieur
404
- const arrowSize = size * 0.15;
405
- ctx.fillStyle = '#000';
406
-
407
- // Flèche haut
408
355
  ctx.beginPath();
409
- ctx.moveTo(x + size/2, y + pad - 2);
410
- ctx.lineTo(x + size/2 - arrowSize, y + pad + arrowSize);
411
- ctx.lineTo(x + size/2 + arrowSize, y + pad + arrowSize);
356
+ ctx.moveTo(x + size / 2, y + pad - 2);
357
+ ctx.lineTo(x + size / 2 - arrowSize, y + pad + arrowSize);
358
+ ctx.lineTo(x + size / 2 + arrowSize, y + pad + arrowSize);
412
359
  ctx.fill();
413
-
414
- // Flèche bas
360
+
415
361
  ctx.beginPath();
416
- ctx.moveTo(x + size/2, y + size - pad + 2);
417
- ctx.lineTo(x + size/2 - arrowSize, y + size - pad - arrowSize);
418
- ctx.lineTo(x + size/2 + arrowSize, y + size - pad - arrowSize);
362
+ ctx.moveTo(x + size / 2, y + size - pad + 2);
363
+ ctx.lineTo(x + size / 2 - arrowSize, y + size - pad - arrowSize);
364
+ ctx.lineTo(x + size / 2 + arrowSize, y + size - pad - arrowSize);
419
365
  ctx.fill();
420
-
421
- // Flèche gauche
366
+
422
367
  ctx.beginPath();
423
- ctx.moveTo(x + pad - 2, y + size/2);
424
- ctx.lineTo(x + pad + arrowSize, y + size/2 - arrowSize);
425
- ctx.lineTo(x + pad + arrowSize, y + size/2 + arrowSize);
368
+ ctx.moveTo(x + pad - 2, y + size / 2);
369
+ ctx.lineTo(x + pad + arrowSize, y + size / 2 - arrowSize);
370
+ ctx.lineTo(x + pad + arrowSize, y + size / 2 + arrowSize);
426
371
  ctx.fill();
427
-
428
- // Flèche droite
372
+
429
373
  ctx.beginPath();
430
- ctx.moveTo(x + size - pad + 2, y + size/2);
431
- ctx.lineTo(x + size - pad - arrowSize, y + size/2 - arrowSize);
432
- ctx.lineTo(x + size - pad - arrowSize, y + size/2 + arrowSize);
374
+ ctx.moveTo(x + size - pad + 2, y + size / 2);
375
+ ctx.lineTo(x + size - pad - arrowSize, y + size / 2 - arrowSize);
376
+ ctx.lineTo(x + size - pad - arrowSize, y + size / 2 + arrowSize);
433
377
  ctx.fill();
434
378
  }
435
379
 
436
380
  drawCoverIcon(ctx, x, y, size) {
437
- // Icône "cover" : rectangle avec flèches vers l'extérieur
438
381
  const pad = size * 0.2;
439
382
  ctx.strokeStyle = '#000';
440
383
  ctx.lineWidth = 2;
441
-
442
- // Rectangle intérieur
443
384
  ctx.strokeRect(x + pad, y + pad, size - pad * 2, size - pad * 2);
444
-
445
- // Flèches pointant vers l'extérieur
385
+
446
386
  const arrowSize = size * 0.15;
447
387
  ctx.fillStyle = '#000';
448
-
449
- // Flèche haut
388
+
450
389
  ctx.beginPath();
451
- ctx.moveTo(x + size/2, y + 2);
452
- ctx.lineTo(x + size/2 - arrowSize, y + arrowSize + 2);
453
- ctx.lineTo(x + size/2 + arrowSize, y + arrowSize + 2);
390
+ ctx.moveTo(x + size / 2, y + 2);
391
+ ctx.lineTo(x + size / 2 - arrowSize, y + arrowSize + 2);
392
+ ctx.lineTo(x + size / 2 + arrowSize, y + arrowSize + 2);
454
393
  ctx.fill();
455
-
456
- // Flèche bas
394
+
457
395
  ctx.beginPath();
458
- ctx.moveTo(x + size/2, y + size - 2);
459
- ctx.lineTo(x + size/2 - arrowSize, y + size - arrowSize - 2);
460
- ctx.lineTo(x + size/2 + arrowSize, y + size - arrowSize - 2);
396
+ ctx.moveTo(x + size / 2, y + size - 2);
397
+ ctx.lineTo(x + size / 2 - arrowSize, y + size - arrowSize - 2);
398
+ ctx.lineTo(x + size / 2 + arrowSize, y + size - arrowSize - 2);
461
399
  ctx.fill();
462
-
463
- // Flèche gauche
400
+
464
401
  ctx.beginPath();
465
- ctx.moveTo(x + 2, y + size/2);
466
- ctx.lineTo(x + arrowSize + 2, y + size/2 - arrowSize);
467
- ctx.lineTo(x + arrowSize + 2, y + size/2 + arrowSize);
402
+ ctx.moveTo(x + 2, y + size / 2);
403
+ ctx.lineTo(x + arrowSize + 2, y + size / 2 - arrowSize);
404
+ ctx.lineTo(x + arrowSize + 2, y + size / 2 + arrowSize);
468
405
  ctx.fill();
469
-
470
- // Flèche droite
406
+
471
407
  ctx.beginPath();
472
- ctx.moveTo(x + size - 2, y + size/2);
473
- ctx.lineTo(x + size - arrowSize - 2, y + size/2 - arrowSize);
474
- ctx.lineTo(x + size - arrowSize - 2, y + size/2 + arrowSize);
408
+ ctx.moveTo(x + size - 2, y + size / 2);
409
+ ctx.lineTo(x + size - arrowSize - 2, y + size / 2 - arrowSize);
410
+ ctx.lineTo(x + size - arrowSize - 2, y + size / 2 + arrowSize);
475
411
  ctx.fill();
476
412
  }
477
413
 
478
414
  drawSwitchCameraIcon(ctx, x, y, size) {
479
- // Icône de switch caméra : deux caméras avec flèche circulaire
480
415
  const centerX = x + size / 2;
481
416
  const centerY = y + size / 2;
482
417
  const radius = size * 0.35;
483
-
418
+
484
419
  ctx.strokeStyle = '#ffffff';
485
420
  ctx.lineWidth = 3;
486
421
  ctx.lineCap = 'round';
487
-
488
- // Arc circulaire (flèche de rotation)
422
+
489
423
  ctx.beginPath();
490
424
  ctx.arc(centerX, centerY, radius, -Math.PI * 0.7, Math.PI * 0.7);
491
425
  ctx.stroke();
492
-
493
- // Flèche en haut à droite
426
+
494
427
  const arrowSize = size * 0.15;
495
428
  const arrowAngle = Math.PI * 0.7;
496
429
  const arrowX = centerX + radius * Math.cos(arrowAngle);
497
430
  const arrowY = centerY + radius * Math.sin(arrowAngle);
498
-
431
+
499
432
  ctx.fillStyle = '#ffffff';
500
433
  ctx.beginPath();
501
434
  ctx.moveTo(arrowX, arrowY);
502
435
  ctx.lineTo(arrowX - arrowSize, arrowY - arrowSize * 0.5);
503
436
  ctx.lineTo(arrowX - arrowSize * 0.5, arrowY + arrowSize);
504
437
  ctx.fill();
505
-
506
- // Mini caméra au centre
438
+
507
439
  const camWidth = size * 0.25;
508
440
  const camHeight = size * 0.18;
509
- const camX = centerX - camWidth / 2;
510
- const camY = centerY - camHeight / 2;
511
-
512
441
  ctx.fillStyle = '#ffffff';
513
- ctx.fillRect(camX, camY, camWidth, camHeight);
514
-
515
- // Objectif
442
+ ctx.fillRect(centerX - camWidth / 2, centerY - camHeight / 2, camWidth, camHeight);
443
+
516
444
  ctx.fillStyle = '#000';
517
445
  ctx.beginPath();
518
446
  ctx.arc(centerX, centerY, size * 0.08, 0, Math.PI * 2);
519
447
  ctx.fill();
520
448
  }
521
449
 
450
+ drawTorchIcon(ctx, x, y, size, torchOn) {
451
+ const cx = x + size / 2;
452
+ const cy = y + size / 2;
453
+
454
+ ctx.fillStyle = torchOn ? 'rgba(255, 235, 59, 0.9)' : 'rgba(0, 0, 0, 0.55)';
455
+ ctx.beginPath();
456
+ ctx.arc(cx, cy, size / 2, 0, Math.PI * 2);
457
+ ctx.fill();
458
+
459
+ ctx.strokeStyle = torchOn ? '#f9a825' : 'rgba(255,255,255,0.3)';
460
+ ctx.lineWidth = 1.5;
461
+ ctx.beginPath();
462
+ ctx.arc(cx, cy, size / 2, 0, Math.PI * 2);
463
+ ctx.stroke();
464
+
465
+ ctx.font = `${Math.round(size * 0.48)}px Arial`;
466
+ ctx.textAlign = 'center';
467
+ ctx.textBaseline = 'middle';
468
+ ctx.fillStyle = torchOn ? '#000000' : '#ffffff';
469
+ ctx.fillText('⚡', cx, cy);
470
+ }
471
+
472
+ // ─── Rendu ──────────────────────────────────────────────────────────────────
473
+
522
474
  draw(ctx) {
523
475
  ctx.save();
524
476
 
525
477
  ctx.fillStyle = '#000';
526
478
  ctx.fillRect(this.x, this.y, this.width, this.height);
527
479
 
528
- // Flash blanc après capture
529
480
  if (this.flashTimer) {
530
481
  ctx.fillStyle = 'rgba(255,255,255,0.6)';
531
482
  ctx.fillRect(this.x, this.y, this.width, this.height);
@@ -536,13 +487,15 @@ class FloatedCamera extends Component {
536
487
  ctx.font = '16px Arial';
537
488
  ctx.textAlign = 'center';
538
489
  ctx.textBaseline = 'middle';
539
- ctx.fillText(this.error, this.x + this.width/2, this.y + this.height/2);
490
+ ctx.fillText(this.error, this.x + this.width / 2, this.y + this.height / 2);
491
+
540
492
  } else if (!this.loaded) {
541
493
  ctx.fillStyle = '#fff';
542
494
  ctx.font = '16px Arial';
543
495
  ctx.textAlign = 'center';
544
496
  ctx.textBaseline = 'middle';
545
- ctx.fillText('Démarrage caméra...', this.x + this.width/2, this.y + this.height/2);
497
+ ctx.fillText('Démarrage caméra...', this.x + this.width / 2, this.y + this.height / 2);
498
+
546
499
  } else if (this.video && this.loaded) {
547
500
  const videoRatio = this.video.videoWidth / this.video.videoHeight;
548
501
  const canvasRatio = this.width / this.height;
@@ -572,76 +525,68 @@ class FloatedCamera extends Component {
572
525
  drawWidth = drawHeight * videoRatio;
573
526
  offsetX = (this.width - drawWidth) / 2;
574
527
  }
575
- }
528
+ }
576
529
 
577
530
  ctx.drawImage(this.video, this.x + offsetX, this.y + offsetY, drawWidth, drawHeight);
578
531
 
579
- // Mini preview dernière photo (bas droite, 3s)
580
532
  if (this.previewPhoto) {
581
533
  const previewSize = 80;
582
534
  const img = new Image();
583
535
  img.src = this.previewPhoto;
584
- ctx.drawImage(img, this.x + this.width - previewSize - 10, this.y + this.height - previewSize - 10, previewSize, previewSize);
536
+ const px = this.x + this.width - previewSize - 10;
537
+ const py = this.y + this.height - previewSize - 10;
538
+ ctx.drawImage(img, px, py, previewSize, previewSize);
585
539
  ctx.strokeStyle = '#fff';
586
540
  ctx.lineWidth = 2;
587
- ctx.strokeRect(this.x + this.width - previewSize - 10, this.y + this.height - previewSize - 10, previewSize, previewSize);
541
+ ctx.strokeRect(px, py, previewSize, previewSize);
588
542
  }
589
543
  }
590
544
 
591
- // Contrôles bas
545
+ // ── Barre bas ───────────────────────────────────────────────────────────
592
546
  ctx.fillStyle = 'rgba(0,0,0,0.5)';
593
547
  ctx.fillRect(this.x, this.y + this.height - 100, this.width, 100);
594
548
 
595
549
  // Bouton capture
550
+ const { cx, cy } = this._getCaptureBtnCenter();
596
551
  ctx.fillStyle = '#ffffff';
597
552
  ctx.beginPath();
598
- ctx.arc(this.x + this.width/2, this.y + this.height - 50, this.captureButtonRadius, 0, Math.PI * 2);
553
+ ctx.arc(this.x + cx, this.y + cy, this.captureButtonRadius, 0, Math.PI * 2);
599
554
  ctx.fill();
600
555
 
601
556
  ctx.strokeStyle = '#ff4444';
602
557
  ctx.lineWidth = 6;
603
558
  ctx.beginPath();
604
- ctx.arc(this.x + this.width/2, this.y + this.height - 50, this.captureButtonRadius + 10, 0, Math.PI * 2);
559
+ ctx.arc(this.x + cx, this.y + cy, this.captureButtonRadius + 10, 0, Math.PI * 2);
605
560
  ctx.stroke();
606
561
 
607
- // Switch caméra avec icône
608
- const switchBtnX = this.x + 20;
609
- const switchBtnY = this.y + 20;
610
- const switchBtnSize = 50;
611
-
612
- // Fond semi-transparent
562
+ // ── Boutons haut ────────────────────────────────────────────────────────
563
+ // Ordre de dessin : switch → mode → torch (torch EN DERNIER = jamais caché)
564
+
565
+ // 1. Switch caméra (GAUCHE)
566
+ const sw = this._getSwitchBtnBounds();
613
567
  ctx.fillStyle = 'rgba(0,0,0,0.5)';
614
568
  ctx.beginPath();
615
- ctx.arc(switchBtnX + switchBtnSize/2, switchBtnY + switchBtnSize/2, switchBtnSize/2, 0, Math.PI * 2);
569
+ ctx.arc(this.x + sw.x + sw.size / 2, this.y + sw.y + sw.size / 2, sw.size / 2, 0, Math.PI * 2);
616
570
  ctx.fill();
617
-
618
- // Icône
619
- this.drawSwitchCameraIcon(ctx, switchBtnX, switchBtnY, switchBtnSize);
571
+ this.drawSwitchCameraIcon(ctx, this.x + sw.x, this.y + sw.y, sw.size);
620
572
 
621
- // Torch
622
- if (this.torchSupported) {
623
- ctx.fillStyle = this.torchOn ? '#ffeb3b' : '#ffffff';
624
- ctx.fillText('⚡', this.x + this.width - 50, this.y + 45);
625
- }
626
-
627
- // Bouton switch mode avec icône
628
- const btnX = this.x + this.width - 80;
629
- const btnY = this.y + 20;
630
-
631
- // Fond du bouton
573
+ // 2. Mode contain/cover (DROITE)
574
+ const mb = this._getModeBtnBounds();
632
575
  ctx.fillStyle = 'rgba(255,255,255,0.9)';
633
- ctx.fillRect(btnX, btnY, this.modeButtonSize, this.modeButtonSize);
634
-
635
- // Bordure
576
+ ctx.fillRect(this.x + mb.x, this.y + mb.y, mb.size, mb.size);
636
577
  ctx.strokeStyle = '#000';
637
578
  ctx.lineWidth = 2;
638
- ctx.strokeRect(btnX, btnY, this.modeButtonSize, this.modeButtonSize);
639
-
640
- // Dessiner l'icône appropriée
579
+ ctx.strokeRect(this.x + mb.x, this.y + mb.y, mb.size, mb.size);
641
580
  if (this.fitMode === 'contain') {
642
- this.drawContainIcon(ctx, btnX, btnY, this.modeButtonSize);
581
+ this.drawContainIcon(ctx, this.x + mb.x, this.y + mb.y, mb.size);
643
582
  } else {
644
- this.drawCoverIcon(ctx, btnX, btnY, this.modeButtonSize);
583
+ this.drawCoverIcon(ctx, this.x + mb.x, this.y + mb.y, mb.size);
584
+ }
585
+
586
+ // 3. Torch (CENTRE) — dessiné en dernier, jamais caché par les autres
587
+ if (this.torchSupported) {
588
+ const tb = this._getTorchBtnBounds();
589
+ this.drawTorchIcon(ctx, this.x + tb.x, this.y + tb.y, tb.size, this.torchOn);
645
590
  }
646
591
 
647
592
  ctx.restore();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasframework",
3
- "version": "0.5.60",
3
+ "version": "0.5.61",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/beyons/CanvasFramework.git"