canvasframework 0.6.0 → 0.6.1

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.
@@ -2,7 +2,9 @@ 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]
7
+ * Layout bouton bas : [capture CENTRE]
6
8
  */
7
9
  class Camera extends Component {
8
10
  constructor(framework, options = {}) {
@@ -29,55 +31,56 @@ class Camera extends Component {
29
31
 
30
32
  // Feedback capture
31
33
  this.flashTimer = null;
32
- this.previewPhoto = null; // dataUrl dernière photo
34
+ this.previewPhoto = null;
35
+ this._previewImg = null; // image pré-chargée
33
36
  this.previewTimeout = null;
34
37
 
35
38
  this.isStarting = false;
39
+
40
+ // Taille et position des boutons (haut)
41
+ this.topBtnSize = 50;
42
+ this.topBtnMargin = 20;
36
43
  }
37
-
44
+
38
45
  static cleanupAllCameras() {
39
46
  const allVideos = document.querySelectorAll('video');
40
47
  allVideos.forEach(video => {
41
- // Arrêter les streams
42
48
  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
49
+ const stream = video.srcObject;
50
+ if (stream && stream.getTracks) {
51
+ stream.getTracks().forEach(track => track.stop());
52
+ }
53
+ video.srcObject = null;
54
+ }
51
55
  if (video.parentNode) {
52
- video.parentNode.removeChild(video);
56
+ video.parentNode.removeChild(video);
53
57
  }
54
58
  });
55
59
  }
56
60
 
57
61
  async _mount() {
58
- super._mount?.();
59
-
60
- // ✅ CORRECTION : Ne démarrer que si visible ET pas en navigation
61
- if (this.visible &&
62
- this.autoStart &&
63
- !this.stream &&
64
- !this.isStarting &&
65
- !this.framework._isNavigating) {
66
- this.isStarting = true;
67
- await this.startCamera();
68
- this.isStarting = false;
69
- }
70
-
71
- this.setupEventListeners();
62
+ super._mount?.();
63
+
64
+ if (this.visible &&
65
+ this.autoStart &&
66
+ !this.stream &&
67
+ !this.isStarting &&
68
+ !this.framework._isNavigating) {
69
+ this.isStarting = true;
70
+ await this.startCamera();
71
+ this.isStarting = false;
72
+ }
73
+
74
+ this.setupEventListeners();
72
75
  }
73
-
76
+
74
77
  onUnmount() {
75
78
  this.removeEventListeners();
76
79
  this.stopCamera();
77
80
  if (this.flashTimer) clearTimeout(this.flashTimer);
78
81
  if (this.previewTimeout) clearTimeout(this.previewTimeout);
79
82
  }
80
-
83
+
81
84
  destroy() {
82
85
  this.removeEventListeners();
83
86
  this.stopCamera();
@@ -86,35 +89,35 @@ class Camera extends Component {
86
89
  super.destroy?.();
87
90
  }
88
91
 
89
- // Écoute directe (indépendante du framework)
92
+ // ─── Événements ─────────────────────────────────────────────────────────────
93
+
90
94
  setupEventListeners() {
91
95
  this.onTouchStart = this.handleTouchStart.bind(this);
92
- this.onTouchMove = this.handleTouchMove.bind(this);
93
- this.onTouchEnd = this.handleTouchEnd.bind(this);
94
- this.onMouseDown = this.handleMouseDown.bind(this);
95
- this.onMouseMove = this.handleMouseMove.bind(this);
96
- this.onMouseUp = this.handleMouseUp.bind(this);
96
+ this.onTouchMove = this.handleTouchMove.bind(this);
97
+ this.onTouchEnd = this.handleTouchEnd.bind(this);
98
+ this.onMouseDown = this.handleMouseDown.bind(this);
99
+ this.onMouseMove = this.handleMouseMove.bind(this);
100
+ this.onMouseUp = this.handleMouseUp.bind(this);
97
101
 
98
102
  const canvas = this.framework.canvas;
99
103
  canvas.addEventListener('touchstart', this.onTouchStart, { passive: false });
100
- canvas.addEventListener('touchmove', this.onTouchMove, { passive: false });
101
- canvas.addEventListener('touchend', this.onTouchEnd, { passive: false });
102
- canvas.addEventListener('mousedown', this.onMouseDown);
103
- canvas.addEventListener('mousemove', this.onMouseMove);
104
- canvas.addEventListener('mouseup', this.onMouseUp);
104
+ canvas.addEventListener('touchmove', this.onTouchMove, { passive: false });
105
+ canvas.addEventListener('touchend', this.onTouchEnd, { passive: false });
106
+ canvas.addEventListener('mousedown', this.onMouseDown);
107
+ canvas.addEventListener('mousemove', this.onMouseMove);
108
+ canvas.addEventListener('mouseup', this.onMouseUp);
105
109
  }
106
110
 
107
111
  removeEventListeners() {
108
112
  const canvas = this.framework.canvas;
109
113
  canvas.removeEventListener('touchstart', this.onTouchStart);
110
- canvas.removeEventListener('touchmove', this.onTouchMove);
111
- canvas.removeEventListener('touchend', this.onTouchEnd);
112
- canvas.removeEventListener('mousedown', this.onMouseDown);
113
- canvas.removeEventListener('mousemove', this.onMouseMove);
114
- canvas.removeEventListener('mouseup', this.onMouseUp);
114
+ canvas.removeEventListener('touchmove', this.onTouchMove);
115
+ canvas.removeEventListener('touchend', this.onTouchEnd);
116
+ canvas.removeEventListener('mousedown', this.onMouseDown);
117
+ canvas.removeEventListener('mousemove', this.onMouseMove);
118
+ canvas.removeEventListener('mouseup', this.onMouseUp);
115
119
  }
116
120
 
117
- // Coordonnées locales
118
121
  getLocalPos(clientX, clientY) {
119
122
  const rect = this.framework.canvas.getBoundingClientRect();
120
123
  const globalX = clientX - rect.left;
@@ -132,13 +135,8 @@ class Camera extends Component {
132
135
  this.handlePress(pos.x, pos.y);
133
136
  }
134
137
 
135
- handleTouchMove(e) {
136
- e.preventDefault();
137
- }
138
-
139
- handleTouchEnd(e) {
140
- e.preventDefault();
141
- }
138
+ handleTouchMove(e) { e.preventDefault(); }
139
+ handleTouchEnd(e) { e.preventDefault(); }
142
140
 
143
141
  handleMouseDown(e) {
144
142
  const pos = this.getLocalPos(e.clientX, e.clientY);
@@ -146,11 +144,12 @@ class Camera extends Component {
146
144
  }
147
145
 
148
146
  handleMouseMove(e) {}
147
+ handleMouseUp(e) {}
149
148
 
150
- handleMouseUp(e) {}
149
+ // ─── Caméra ─────────────────────────────────────────────────────────────────
151
150
 
152
151
  async startCamera() {
153
- Camera.cleanupAllCameras();
152
+ Camera.cleanupAllCameras();
154
153
  if (this.stream) return;
155
154
 
156
155
  try {
@@ -159,8 +158,8 @@ class Camera extends Component {
159
158
  this.stream = await navigator.mediaDevices.getUserMedia({
160
159
  video: {
161
160
  facingMode: this.facingMode,
162
- width: { ideal: 1280 },
163
- height: { ideal: 720 },
161
+ width: { ideal: 1280 },
162
+ height: { ideal: 720 },
164
163
  frameRate: { ideal: 30 }
165
164
  }
166
165
  });
@@ -170,16 +169,16 @@ class Camera extends Component {
170
169
  this.torchSupported = !!caps.torch;
171
170
 
172
171
  this.video = document.createElement('video');
173
- this.video.autoplay = true;
172
+ this.video.autoplay = true;
174
173
  this.video.playsInline = true;
175
- this.video.muted = true;
176
- this.video.srcObject = this.stream;
174
+ this.video.muted = true;
175
+ this.video.srcObject = this.stream;
177
176
 
178
177
  this.video.style.position = 'fixed';
179
- this.video.style.left = '-9999px';
180
- this.video.style.top = '-9999px';
181
- this.video.style.width = '1px';
182
- this.video.style.height = '1px';
178
+ this.video.style.left = '-9999px';
179
+ this.video.style.top = '-9999px';
180
+ this.video.style.width = '1px';
181
+ this.video.style.height = '1px';
183
182
  document.body.appendChild(this.video);
184
183
 
185
184
  await new Promise(resolve => {
@@ -230,9 +229,7 @@ class Camera extends Component {
230
229
 
231
230
  const [track] = this.stream.getVideoTracks();
232
231
  try {
233
- await track.applyConstraints({
234
- advanced: [{ torch: !this.torchOn }]
235
- });
232
+ await track.applyConstraints({ advanced: [{ torch: !this.torchOn }] });
236
233
  this.torchOn = !this.torchOn;
237
234
  this.markDirty();
238
235
  } catch (err) {
@@ -247,7 +244,7 @@ class Camera extends Component {
247
244
  }
248
245
 
249
246
  const canvas = document.createElement('canvas');
250
- canvas.width = this.video.videoWidth;
247
+ canvas.width = this.video.videoWidth;
251
248
  canvas.height = this.video.videoHeight;
252
249
 
253
250
  const ctx = canvas.getContext('2d');
@@ -255,7 +252,6 @@ class Camera extends Component {
255
252
 
256
253
  const dataUrl = canvas.toDataURL('image/jpeg', 0.92);
257
254
  if (this.onPhoto) this.onPhoto(dataUrl);
258
- console.log('Photo capturée ! DataURL:', dataUrl.substring(0, 50) + '...');
259
255
 
260
256
  // Feedback : flash + preview 3s
261
257
  this.flashTimer = setTimeout(() => {
@@ -263,9 +259,14 @@ class Camera extends Component {
263
259
  this.markDirty();
264
260
  }, 200);
265
261
 
262
+ // Pré-charger l'image une seule fois
263
+ this._previewImg = new Image();
264
+ this._previewImg.src = dataUrl;
266
265
  this.previewPhoto = dataUrl;
266
+
267
267
  this.previewTimeout = setTimeout(() => {
268
268
  this.previewPhoto = null;
269
+ this._previewImg = null;
269
270
  this.markDirty();
270
271
  }, 3000);
271
272
 
@@ -279,383 +280,329 @@ class Camera extends Component {
279
280
  this.markDirty();
280
281
  }
281
282
 
282
- handlePress(relX, relY) {
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
- }
283
+ // ─── Zones des boutons ───────────────────────────────────────────────────────
284
+ // Layout haut : [switch caméra GAUCHE] [torch CENTRE] [mode DROITE]
285
+ // Layout bas : [capture CENTRE]
286
+
287
+ _getSwitchBtnBounds() {
288
+ return {
289
+ x: this.topBtnMargin,
290
+ y: this.topBtnMargin,
291
+ size: this.topBtnSize
292
+ };
311
293
  }
312
294
 
313
- drawContainIcon(ctx, x, y, size) {
314
- // Icône "contain" : rectangle avec flèches vers l'intérieur
315
- const pad = size * 0.2;
316
- ctx.strokeStyle = '#000';
317
- ctx.lineWidth = 2;
318
-
319
- // Rectangle extérieur
320
- ctx.strokeRect(x + pad, y + pad, size - pad * 2, size - pad * 2);
321
-
322
- // Flèches pointant vers l'intérieur
323
- const arrowSize = size * 0.15;
324
- ctx.fillStyle = '#000';
325
-
326
- // Flèche haut
327
- ctx.beginPath();
328
- ctx.moveTo(x + size/2, y + pad - 2);
329
- ctx.lineTo(x + size/2 - arrowSize, y + pad + arrowSize);
330
- ctx.lineTo(x + size/2 + arrowSize, y + pad + arrowSize);
331
- ctx.fill();
332
-
333
- // Flèche bas
334
- ctx.beginPath();
335
- ctx.moveTo(x + size/2, y + size - pad + 2);
336
- ctx.lineTo(x + size/2 - arrowSize, y + size - pad - arrowSize);
337
- ctx.lineTo(x + size/2 + arrowSize, y + size - pad - arrowSize);
338
- ctx.fill();
339
-
340
- // Flèche gauche
341
- ctx.beginPath();
342
- ctx.moveTo(x + pad - 2, y + size/2);
343
- ctx.lineTo(x + pad + arrowSize, y + size/2 - arrowSize);
344
- ctx.lineTo(x + pad + arrowSize, y + size/2 + arrowSize);
345
- ctx.fill();
346
-
347
- // Flèche droite
348
- ctx.beginPath();
349
- ctx.moveTo(x + size - pad + 2, y + size/2);
350
- ctx.lineTo(x + size - pad - arrowSize, y + size/2 - arrowSize);
351
- ctx.lineTo(x + size - pad - arrowSize, y + size/2 + arrowSize);
352
- ctx.fill();
295
+ _getTorchBtnBounds() {
296
+ return {
297
+ x: (this.width - this.topBtnSize) / 2,
298
+ y: this.topBtnMargin,
299
+ size: this.topBtnSize
300
+ };
353
301
  }
354
302
 
355
- drawCoverIcon(ctx, x, y, size) {
356
- // Icône "cover" : rectangle avec flèches vers l'extérieur
357
- const pad = size * 0.2;
358
- ctx.strokeStyle = '#000';
359
- ctx.lineWidth = 2;
360
-
361
- // Rectangle intérieur
362
- ctx.strokeRect(x + pad, y + pad, size - pad * 2, size - pad * 2);
363
-
364
- // Flèches pointant vers l'extérieur
365
- const arrowSize = size * 0.15;
366
- ctx.fillStyle = '#000';
367
-
368
- // Flèche haut
369
- ctx.beginPath();
370
- ctx.moveTo(x + size/2, y + 2);
371
- ctx.lineTo(x + size/2 - arrowSize, y + arrowSize + 2);
372
- ctx.lineTo(x + size/2 + arrowSize, y + arrowSize + 2);
373
- ctx.fill();
374
-
375
- // Flèche bas
376
- ctx.beginPath();
377
- ctx.moveTo(x + size/2, y + size - 2);
378
- ctx.lineTo(x + size/2 - arrowSize, y + size - arrowSize - 2);
379
- ctx.lineTo(x + size/2 + arrowSize, y + size - arrowSize - 2);
380
- ctx.fill();
381
-
382
- // Flèche gauche
383
- ctx.beginPath();
384
- ctx.moveTo(x + 2, y + size/2);
385
- ctx.lineTo(x + arrowSize + 2, y + size/2 - arrowSize);
386
- ctx.lineTo(x + arrowSize + 2, y + size/2 + arrowSize);
387
- ctx.fill();
388
-
389
- // Flèche droite
390
- ctx.beginPath();
391
- ctx.moveTo(x + size - 2, y + size/2);
392
- ctx.lineTo(x + size - arrowSize - 2, y + size/2 - arrowSize);
393
- ctx.lineTo(x + size - arrowSize - 2, y + size/2 + arrowSize);
394
- ctx.fill();
303
+ _getModeBtnBounds() {
304
+ const yCenter = this.topBtnMargin + this.topBtnSize / 2;
305
+ return {
306
+ x: this.width - this.topBtnMargin - this.modeButtonSize,
307
+ y: yCenter - this.modeButtonSize / 2,
308
+ size: this.modeButtonSize
309
+ };
310
+ }
311
+
312
+ _getCaptureBtnCenter() {
313
+ return {
314
+ cx: this.width / 2,
315
+ cy: this.height - 88
316
+ };
317
+ }
318
+
319
+ // ─── Gestion des clics ──────────────────────────────────────────────────────
320
+
321
+ handlePress(relX, relY) {
322
+ // Guard : ignorer tout clic hors du composant
323
+ if (relX < 0 || relX > this.width || relY < 0 || relY > this.height) return;
324
+
325
+ // Bouton capture (bas centre)
326
+ const { cx, cy } = this._getCaptureBtnCenter();
327
+ if (Math.hypot(relX - cx, relY - cy) < this.captureButtonRadius + 10) {
328
+ this.capturePhoto();
329
+ return;
330
+ }
331
+
332
+ // Switch caméra (haut gauche)
333
+ const sw = this._getSwitchBtnBounds();
334
+ if (Math.hypot(relX - (sw.x + sw.size / 2), relY - (sw.y + sw.size / 2)) < sw.size / 2 + 5) {
335
+ this.switchCamera();
336
+ return;
337
+ }
338
+
339
+ // Torch (haut centre)
340
+ if (this.torchSupported) {
341
+ const tb = this._getTorchBtnBounds();
342
+ if (Math.hypot(relX - (tb.x + tb.size / 2), relY - (tb.y + tb.size / 2)) < tb.size / 2 + 5) {
343
+ this.toggleTorch();
344
+ return;
345
+ }
346
+ }
347
+
348
+ // Mode contain/cover (haut droite)
349
+ const mb = this._getModeBtnBounds();
350
+ if (relX >= mb.x && relX <= mb.x + mb.size && relY >= mb.y && relY <= mb.y + mb.size) {
351
+ this.switchFitMode();
352
+ return;
353
+ }
395
354
  }
396
355
 
397
- drawContainIcon(ctx, x, y, size) {
398
- // Icône "contain" : rectangle avec flèches vers l'intérieur
356
+ // ─── Icônes ─────────────────────────────────────────────────────────────────
357
+
358
+ drawContainIcon(ctx, x, y, size) {
399
359
  const pad = size * 0.2;
400
360
  ctx.strokeStyle = '#000';
401
361
  ctx.lineWidth = 2;
402
-
403
- // Rectangle extérieur
404
362
  ctx.strokeRect(x + pad, y + pad, size - pad * 2, size - pad * 2);
405
-
406
- // Flèches pointant vers l'intérieur
363
+
407
364
  const arrowSize = size * 0.15;
408
365
  ctx.fillStyle = '#000';
409
-
410
- // Flèche haut
366
+
411
367
  ctx.beginPath();
412
- ctx.moveTo(x + size/2, y + pad - 2);
413
- ctx.lineTo(x + size/2 - arrowSize, y + pad + arrowSize);
414
- ctx.lineTo(x + size/2 + arrowSize, y + pad + arrowSize);
368
+ ctx.moveTo(x + size / 2, y + pad - 2);
369
+ ctx.lineTo(x + size / 2 - arrowSize, y + pad + arrowSize);
370
+ ctx.lineTo(x + size / 2 + arrowSize, y + pad + arrowSize);
415
371
  ctx.fill();
416
-
417
- // Flèche bas
372
+
418
373
  ctx.beginPath();
419
- ctx.moveTo(x + size/2, y + size - pad + 2);
420
- ctx.lineTo(x + size/2 - arrowSize, y + size - pad - arrowSize);
421
- ctx.lineTo(x + size/2 + arrowSize, y + size - pad - arrowSize);
374
+ ctx.moveTo(x + size / 2, y + size - pad + 2);
375
+ ctx.lineTo(x + size / 2 - arrowSize, y + size - pad - arrowSize);
376
+ ctx.lineTo(x + size / 2 + arrowSize, y + size - pad - arrowSize);
422
377
  ctx.fill();
423
-
424
- // Flèche gauche
378
+
425
379
  ctx.beginPath();
426
- ctx.moveTo(x + pad - 2, y + size/2);
427
- ctx.lineTo(x + pad + arrowSize, y + size/2 - arrowSize);
428
- ctx.lineTo(x + pad + arrowSize, y + size/2 + arrowSize);
380
+ ctx.moveTo(x + pad - 2, y + size / 2);
381
+ ctx.lineTo(x + pad + arrowSize, y + size / 2 - arrowSize);
382
+ ctx.lineTo(x + pad + arrowSize, y + size / 2 + arrowSize);
429
383
  ctx.fill();
430
-
431
- // Flèche droite
384
+
432
385
  ctx.beginPath();
433
- ctx.moveTo(x + size - pad + 2, y + size/2);
434
- ctx.lineTo(x + size - pad - arrowSize, y + size/2 - arrowSize);
435
- ctx.lineTo(x + size - pad - arrowSize, y + size/2 + arrowSize);
386
+ ctx.moveTo(x + size - pad + 2, y + size / 2);
387
+ ctx.lineTo(x + size - pad - arrowSize, y + size / 2 - arrowSize);
388
+ ctx.lineTo(x + size - pad - arrowSize, y + size / 2 + arrowSize);
436
389
  ctx.fill();
437
390
  }
438
391
 
439
392
  drawCoverIcon(ctx, x, y, size) {
440
- // Icône "cover" : rectangle avec flèches vers l'extérieur
441
393
  const pad = size * 0.2;
442
394
  ctx.strokeStyle = '#000';
443
395
  ctx.lineWidth = 2;
444
-
445
- // Rectangle intérieur
446
396
  ctx.strokeRect(x + pad, y + pad, size - pad * 2, size - pad * 2);
447
-
448
- // Flèches pointant vers l'extérieur
397
+
449
398
  const arrowSize = size * 0.15;
450
399
  ctx.fillStyle = '#000';
451
-
452
- // Flèche haut
400
+
453
401
  ctx.beginPath();
454
- ctx.moveTo(x + size/2, y + 2);
455
- ctx.lineTo(x + size/2 - arrowSize, y + arrowSize + 2);
456
- ctx.lineTo(x + size/2 + arrowSize, y + arrowSize + 2);
402
+ ctx.moveTo(x + size / 2, y + 2);
403
+ ctx.lineTo(x + size / 2 - arrowSize, y + arrowSize + 2);
404
+ ctx.lineTo(x + size / 2 + arrowSize, y + arrowSize + 2);
457
405
  ctx.fill();
458
-
459
- // Flèche bas
406
+
460
407
  ctx.beginPath();
461
- ctx.moveTo(x + size/2, y + size - 2);
462
- ctx.lineTo(x + size/2 - arrowSize, y + size - arrowSize - 2);
463
- ctx.lineTo(x + size/2 + arrowSize, y + size - arrowSize - 2);
408
+ ctx.moveTo(x + size / 2, y + size - 2);
409
+ ctx.lineTo(x + size / 2 - arrowSize, y + size - arrowSize - 2);
410
+ ctx.lineTo(x + size / 2 + arrowSize, y + size - arrowSize - 2);
464
411
  ctx.fill();
465
-
466
- // Flèche gauche
412
+
467
413
  ctx.beginPath();
468
- ctx.moveTo(x + 2, y + size/2);
469
- ctx.lineTo(x + arrowSize + 2, y + size/2 - arrowSize);
470
- ctx.lineTo(x + arrowSize + 2, y + size/2 + arrowSize);
414
+ ctx.moveTo(x + 2, y + size / 2);
415
+ ctx.lineTo(x + arrowSize + 2, y + size / 2 - arrowSize);
416
+ ctx.lineTo(x + arrowSize + 2, y + size / 2 + arrowSize);
471
417
  ctx.fill();
472
-
473
- // Flèche droite
418
+
474
419
  ctx.beginPath();
475
- ctx.moveTo(x + size - 2, y + size/2);
476
- ctx.lineTo(x + size - arrowSize - 2, y + size/2 - arrowSize);
477
- ctx.lineTo(x + size - arrowSize - 2, y + size/2 + arrowSize);
420
+ ctx.moveTo(x + size - 2, y + size / 2);
421
+ ctx.lineTo(x + size - arrowSize - 2, y + size / 2 - arrowSize);
422
+ ctx.lineTo(x + size - arrowSize - 2, y + size / 2 + arrowSize);
478
423
  ctx.fill();
479
424
  }
480
425
 
481
426
  drawSwitchCameraIcon(ctx, x, y, size) {
482
- // Icône de switch caméra : deux caméras avec flèche circulaire
483
427
  const centerX = x + size / 2;
484
428
  const centerY = y + size / 2;
485
- const radius = size * 0.35;
486
-
429
+ const radius = size * 0.35;
430
+
487
431
  ctx.strokeStyle = '#ffffff';
488
- ctx.lineWidth = 3;
489
- ctx.lineCap = 'round';
490
-
491
- // Arc circulaire (flèche de rotation)
432
+ ctx.lineWidth = 3;
433
+ ctx.lineCap = 'round';
434
+
492
435
  ctx.beginPath();
493
436
  ctx.arc(centerX, centerY, radius, -Math.PI * 0.7, Math.PI * 0.7);
494
437
  ctx.stroke();
495
-
496
- // Flèche en haut à droite
497
- const arrowSize = size * 0.15;
438
+
439
+ const arrowSize = size * 0.15;
498
440
  const arrowAngle = Math.PI * 0.7;
499
- const arrowX = centerX + radius * Math.cos(arrowAngle);
500
- const arrowY = centerY + radius * Math.sin(arrowAngle);
501
-
441
+ const arrowX = centerX + radius * Math.cos(arrowAngle);
442
+ const arrowY = centerY + radius * Math.sin(arrowAngle);
443
+
502
444
  ctx.fillStyle = '#ffffff';
503
445
  ctx.beginPath();
504
446
  ctx.moveTo(arrowX, arrowY);
505
447
  ctx.lineTo(arrowX - arrowSize, arrowY - arrowSize * 0.5);
506
448
  ctx.lineTo(arrowX - arrowSize * 0.5, arrowY + arrowSize);
507
449
  ctx.fill();
508
-
509
- // Mini caméra au centre
510
- const camWidth = size * 0.25;
450
+
451
+ const camWidth = size * 0.25;
511
452
  const camHeight = size * 0.18;
512
- const camX = centerX - camWidth / 2;
513
- const camY = centerY - camHeight / 2;
514
-
515
453
  ctx.fillStyle = '#ffffff';
516
- ctx.fillRect(camX, camY, camWidth, camHeight);
517
-
518
- // Objectif
454
+ ctx.fillRect(centerX - camWidth / 2, centerY - camHeight / 2, camWidth, camHeight);
455
+
519
456
  ctx.fillStyle = '#000';
520
457
  ctx.beginPath();
521
458
  ctx.arc(centerX, centerY, size * 0.08, 0, Math.PI * 2);
522
459
  ctx.fill();
523
460
  }
524
461
 
525
- draw(ctx) {
526
- ctx.save();
462
+ drawTorchIcon(ctx, x, y, size, torchOn) {
463
+ const cx = x + size / 2;
464
+ const cy = y + size / 2;
527
465
 
528
- ctx.fillStyle = '#000';
529
- ctx.fillRect(this.x, this.y, this.width, this.height);
466
+ ctx.fillStyle = torchOn ? 'rgba(255, 235, 59, 0.9)' : 'rgba(0, 0, 0, 0.55)';
467
+ ctx.beginPath();
468
+ ctx.arc(cx, cy, size / 2, 0, Math.PI * 2);
469
+ ctx.fill();
530
470
 
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);
471
+ ctx.strokeStyle = torchOn ? '#f9a825' : 'rgba(255,255,255,0.3)';
472
+ ctx.lineWidth = 1.5;
473
+ ctx.beginPath();
474
+ ctx.arc(cx, cy, size / 2, 0, Math.PI * 2);
475
+ ctx.stroke();
476
+
477
+ ctx.font = `${Math.round(size * 0.48)}px Arial`;
478
+ ctx.textAlign = 'center';
479
+ ctx.textBaseline = 'middle';
480
+ ctx.fillStyle = torchOn ? '#000000' : '#ffffff';
481
+ ctx.fillText('⚡', cx, cy);
535
482
  }
536
483
 
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;
484
+ // ─── Rendu ──────────────────────────────────────────────────────────────────
485
+
486
+ draw(ctx) {
487
+ ctx.save();
488
+
489
+ // Fond noir
490
+ ctx.fillStyle = '#000';
491
+ ctx.fillRect(this.x, this.y, this.width, this.height);
492
+
493
+ // Flash blanc après capture
494
+ if (this.flashTimer) {
495
+ ctx.fillStyle = 'rgba(255,255,255,0.6)';
496
+ ctx.fillRect(this.x, this.y, this.width, this.height);
497
+ }
498
+
499
+ // États erreur / chargement / vidéo
500
+ if (this.error) {
501
+ ctx.fillStyle = '#ff4444';
502
+ ctx.font = '16px Arial';
503
+ ctx.textAlign = 'center';
504
+ ctx.textBaseline = 'middle';
505
+ ctx.fillText(this.error, this.x + this.width / 2, this.y + this.height / 2);
506
+
507
+ } else if (!this.loaded) {
508
+ ctx.fillStyle = '#fff';
509
+ ctx.font = '16px Arial';
510
+ ctx.textAlign = 'center';
511
+ ctx.textBaseline = 'middle';
512
+ ctx.fillText('Démarrage caméra...', this.x + this.width / 2, this.y + this.height / 2);
513
+
514
+ } else if (this.video && this.loaded) {
515
+ const videoRatio = this.video.videoWidth / this.video.videoHeight;
516
+ const canvasRatio = this.width / this.height;
517
+
518
+ let drawWidth = this.width;
519
+ let drawHeight = this.height;
520
+ let offsetX = 0;
521
+ let offsetY = 0;
522
+
523
+ if (this.fitMode === 'cover') {
524
+ if (videoRatio > canvasRatio) {
525
+ drawHeight = this.height;
526
+ drawWidth = drawHeight * videoRatio;
527
+ offsetX = (this.width - drawWidth) / 2;
528
+ } else {
529
+ drawWidth = this.width;
530
+ drawHeight = drawWidth / videoRatio;
531
+ offsetY = (this.height - drawHeight) / 2;
532
+ }
533
+ } else if (this.fitMode === 'contain') {
534
+ if (videoRatio > canvasRatio) {
535
+ drawWidth = this.width;
536
+ drawHeight = drawWidth / videoRatio;
537
+ offsetY = (this.height - drawHeight) / 2;
538
+ } else {
539
+ drawHeight = this.height;
540
+ drawWidth = drawHeight * videoRatio;
541
+ offsetX = (this.width - drawWidth) / 2;
542
+ }
567
543
  }
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;
544
+
545
+ ctx.drawImage(this.video, this.x + offsetX, this.y + offsetY, drawWidth, drawHeight);
546
+
547
+ // Mini preview dernière photo (bas droite, 3s)
548
+ if (this.previewPhoto && this._previewImg?.complete) {
549
+ const previewSize = 80;
550
+ const px = this.x + this.width - previewSize - 10;
551
+ const py = this.y + this.height - previewSize - 10;
552
+ ctx.drawImage(this._previewImg, px, py, previewSize, previewSize);
553
+ ctx.strokeStyle = '#fff';
554
+ ctx.lineWidth = 2;
555
+ ctx.strokeRect(px, py, previewSize, previewSize);
577
556
  }
578
557
  }
579
558
 
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
- }
559
+ // ── Barre bas ───────────────────────────────────────────────────────────
560
+ ctx.fillStyle = 'rgba(0,0,0,0.5)';
561
+ ctx.fillRect(this.x, this.y + this.height - 100, this.width, 100);
593
562
 
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)';
563
+ // Bouton capture (bas centre)
564
+ const { cx, cy } = this._getCaptureBtnCenter();
565
+ ctx.fillStyle = '#ffffff';
629
566
  ctx.beginPath();
630
- ctx.arc(torchBtnX + torchBtnSize/2, torchBtnY + torchBtnSize/2, torchBtnSize/2, 0, Math.PI * 2);
567
+ ctx.arc(this.x + cx, this.y + cy, this.captureButtonRadius, 0, Math.PI * 2);
631
568
  ctx.fill();
632
569
 
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
- }
570
+ ctx.strokeStyle = '#ff4444';
571
+ ctx.lineWidth = 6;
572
+ ctx.beginPath();
573
+ ctx.arc(this.x + cx, this.y + cy, this.captureButtonRadius + 10, 0, Math.PI * 2);
574
+ ctx.stroke();
639
575
 
640
- // Bouton switch mode (décalé à gauche de la torche)
641
- const btnX = this.x + this.width - 130;
642
- const btnY = this.y + 20;
576
+ // ── Boutons haut ────────────────────────────────────────────────────────
643
577
 
644
- ctx.fillStyle = 'rgba(255,255,255,0.9)';
645
- ctx.fillRect(btnX, btnY, this.modeButtonSize, this.modeButtonSize);
578
+ // 1. Switch caméra (GAUCHE)
579
+ const sw = this._getSwitchBtnBounds();
580
+ ctx.fillStyle = 'rgba(0,0,0,0.5)';
581
+ ctx.beginPath();
582
+ ctx.arc(this.x + sw.x + sw.size / 2, this.y + sw.y + sw.size / 2, sw.size / 2, 0, Math.PI * 2);
583
+ ctx.fill();
584
+ this.drawSwitchCameraIcon(ctx, this.x + sw.x, this.y + sw.y, sw.size);
646
585
 
647
- ctx.strokeStyle = '#000';
648
- ctx.lineWidth = 2;
649
- ctx.strokeRect(btnX, btnY, this.modeButtonSize, this.modeButtonSize);
586
+ // 2. Mode contain/cover (DROITE)
587
+ const mb = this._getModeBtnBounds();
588
+ ctx.fillStyle = 'rgba(255,255,255,0.9)';
589
+ ctx.beginPath();
590
+ ctx.arc(this.x + mb.x + mb.size / 2, this.y + mb.y + mb.size / 2, mb.size / 2, 0, Math.PI * 2);
591
+ ctx.fill();
592
+ if (this.fitMode === 'contain') {
593
+ this.drawContainIcon(ctx, this.x + mb.x, this.y + mb.y, mb.size);
594
+ } else {
595
+ this.drawCoverIcon(ctx, this.x + mb.x, this.y + mb.y, mb.size);
596
+ }
650
597
 
651
- if (this.fitMode === 'contain') {
652
- this.drawContainIcon(ctx, btnX, btnY, this.modeButtonSize);
653
- } else {
654
- this.drawCoverIcon(ctx, btnX, btnY, this.modeButtonSize);
655
- }
598
+ // 3. Torch (CENTRE) — dessiné en dernier, jamais caché
599
+ if (this.torchSupported) {
600
+ const tb = this._getTorchBtnBounds();
601
+ this.drawTorchIcon(ctx, this.x + tb.x, this.y + tb.y, tb.size, this.torchOn);
602
+ }
656
603
 
657
- ctx.restore();
658
- }
604
+ ctx.restore();
605
+ }
659
606
  }
660
607
 
661
608
  export default Camera;
@@ -572,11 +572,10 @@ class FloatedCamera extends Component {
572
572
 
573
573
  // 2. Mode contain/cover (DROITE)
574
574
  const mb = this._getModeBtnBounds();
575
- ctx.fillStyle = 'rgba(255,255,255,0.9)';
576
- ctx.fillRect(this.x + mb.x, this.y + mb.y, mb.size, mb.size);
577
- ctx.strokeStyle = '#000';
578
- ctx.lineWidth = 2;
579
- ctx.strokeRect(this.x + mb.x, this.y + mb.y, mb.size, mb.size);
575
+ ctx.fillStyle = 'rgba(255,255,255,0.9)';
576
+ ctx.beginPath();
577
+ ctx.arc(this.x + mb.x + mb.size / 2, this.y + mb.y + mb.size / 2, mb.size / 2, 0, Math.PI * 2);
578
+ ctx.fill();
580
579
  if (this.fitMode === 'contain') {
581
580
  this.drawContainIcon(ctx, this.x + mb.x, this.y + mb.y, mb.size);
582
581
  } else {
@@ -585,6 +585,7 @@ class CanvasFramework {
585
585
 
586
586
  this.setupEventListeners();
587
587
  this.setupHistoryListener();
588
+ this._initNavigationGuard(); // ← AJOUTER
588
589
 
589
590
  this.startRenderLoop();
590
591
 
@@ -635,6 +636,13 @@ class CanvasFramework {
635
636
  }
636
637
 
637
638
  }
639
+
640
+ _initNavigationGuard() {
641
+ // Injecter 2 entrées : une base + la route actuelle
642
+ // Ainsi le navigateur a toujours une entrée "avant" à consommer
643
+ window.history.replaceState({ route: '/', _guard: true }, '', '/');
644
+ window.history.pushState({ route: '/', _app: true }, '', '/');
645
+ }
638
646
 
639
647
  /**
640
648
  * Crée un élément DOM temporaire, l'ajoute au body, exécute une callback, puis le supprime
@@ -1963,16 +1971,51 @@ class CanvasFramework {
1963
1971
  * @private
1964
1972
  */
1965
1973
  setupHistoryListener() {
1966
- window.addEventListener('popstate', (e) => {
1967
- if (e.state && e.state.route) {
1968
- this.navigateTo(e.state.route, {
1969
- replace: true,
1970
- animate: true,
1971
- direction: 'back'
1972
- });
1973
- }
1974
- });
1975
- }
1974
+ window.addEventListener('popstate', (e) => {
1975
+ // Cas 1 : L'app a un historique interne → naviguer en arrière dans l'app
1976
+ if (this.historyIndex > 0) {
1977
+ this.historyIndex--;
1978
+ const entry = this.history[this.historyIndex];
1979
+
1980
+ // Recharger la vue précédente sans toucher window.history
1981
+ this._navigateInternal(entry.path, {
1982
+ replace: true,
1983
+ animate: true,
1984
+ direction: 'back'
1985
+ });
1986
+
1987
+ // Réinjecter une entrée pour ne jamais vider la pile navigateur
1988
+ window.history.pushState({ route: entry.path, _app: true }, '', entry.path);
1989
+
1990
+ } else {
1991
+ // Cas 2 : Plus d'historique interne → réinjecter et ignorer
1992
+ // (empêche de quitter l'app)
1993
+ const currentPath = this.currentRoute || '/';
1994
+ window.history.pushState({ route: currentPath, _app: true }, '', currentPath);
1995
+
1996
+ // Optionnel : afficher un toast "Appuyez encore pour quitter"
1997
+ this._handleBackExitAttempt();
1998
+ }
1999
+ });
2000
+ }
2001
+
2002
+ _handleBackExitAttempt() {
2003
+ const now = Date.now();
2004
+
2005
+ if (this._lastBackAttempt && (now - this._lastBackAttempt) < 2000) {
2006
+ // Deux back rapides → vraiment quitter (sur PWA/WebView)
2007
+ // Sur navigateur standard ce n'est pas possible, mais sur Capacitor/Cordova oui
2008
+ if (window.navigator.app?.exitApp) {
2009
+ window.navigator.app.exitApp(); // Cordova
2010
+ } else if (window.Capacitor?.Plugins?.App) {
2011
+ window.Capacitor.Plugins.App.exitApp(); // Capacitor
2012
+ }
2013
+ this._lastBackAttempt = null;
2014
+ } else {
2015
+ this._lastBackAttempt = now;
2016
+ this.showToast('Appuyez encore pour quitter', 2000);
2017
+ }
2018
+ }
1976
2019
 
1977
2020
  // ===== MÉTHODES DE ROUTING =====
1978
2021
 
@@ -2207,6 +2250,20 @@ class CanvasFramework {
2207
2250
  }
2208
2251
 
2209
2252
  this._maxScrollDirty = true;
2253
+
2254
+ // Historique navigateur : seulement si ce n'est pas un retour interne
2255
+ if (!options._internal) {
2256
+ if (!replace) {
2257
+ window.history.pushState({ route: path, _app: true }, '', path);
2258
+ } else {
2259
+ window.history.replaceState({ route: path, _app: true }, '', path);
2260
+ }
2261
+ }
2262
+ }
2263
+
2264
+ // Alias pour usage interne (sans toucher window.history)
2265
+ _navigateInternal(path, options = {}) {
2266
+ return this.navigateTo(path, { ...options, _internal: true });
2210
2267
  }
2211
2268
 
2212
2269
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasframework",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/beyons/CanvasFramework.git"