canvasframework 0.5.5 → 0.5.7

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,624 @@
1
+ import Component from '../core/Component.js';
2
+
3
+ /**
4
+ * Composant FloatedCamera autonome avec gestion directe des clics/touches
5
+ * Modes : contain (tout visible + bandes), cover (remplit + crop), fit (centre sans crop)
6
+ */
7
+ class FloatedCamera extends Component {
8
+ constructor(framework, options = {}) {
9
+ super(framework, options);
10
+
11
+ this.facingMode = options.facingMode || 'environment';
12
+ this.autoStart = options.autoStart !== false;
13
+ this.onPhoto = options.onPhoto || null;
14
+
15
+ this.fitModes = ['contain', 'cover'];
16
+ this.currentFitModeIndex = 0;
17
+ this.fitMode = this.fitModes[this.currentFitModeIndex];
18
+
19
+ this.stream = null;
20
+ this.video = null;
21
+ this.loaded = false;
22
+ this.error = null;
23
+
24
+ this.showControls = true;
25
+ this.torchSupported = false;
26
+ this.torchOn = false;
27
+ this.captureButtonRadius = 35;
28
+ this.modeButtonSize = 40;
29
+
30
+ // Feedback capture
31
+ this.flashTimer = null;
32
+ this.previewPhoto = null; // dataUrl dernière photo
33
+ this.previewTimeout = null;
34
+
35
+ this.isStarting = false;
36
+ }
37
+
38
+ async _mount() {
39
+ super._mount?.();
40
+
41
+ // Redémarrage auto si besoin
42
+ if (this.autoStart && !this.stream && !this.isStarting) {
43
+ this.isStarting = true;
44
+ await this.startCamera();
45
+ this.isStarting = false;
46
+ }
47
+
48
+ this.setupEventListeners();
49
+ }
50
+
51
+ destroy() {
52
+ this.removeEventListeners();
53
+ this.stopCamera();
54
+ if (this.flashTimer) clearTimeout(this.flashTimer);
55
+ if (this.previewTimeout) clearTimeout(this.previewTimeout);
56
+ super.destroy?.();
57
+ }
58
+
59
+ // Écoute directe (indépendante du framework)
60
+ setupEventListeners() {
61
+ this.onTouchStart = this.handleTouchStart.bind(this);
62
+ this.onTouchMove = this.handleTouchMove.bind(this);
63
+ this.onTouchEnd = this.handleTouchEnd.bind(this);
64
+ this.onMouseDown = this.handleMouseDown.bind(this);
65
+ this.onMouseMove = this.handleMouseMove.bind(this);
66
+ this.onMouseUp = this.handleMouseUp.bind(this);
67
+
68
+ const canvas = this.framework.canvas;
69
+ canvas.addEventListener('touchstart', this.onTouchStart, { passive: false });
70
+ canvas.addEventListener('touchmove', this.onTouchMove, { passive: false });
71
+ canvas.addEventListener('touchend', this.onTouchEnd, { passive: false });
72
+ canvas.addEventListener('mousedown', this.onMouseDown);
73
+ canvas.addEventListener('mousemove', this.onMouseMove);
74
+ canvas.addEventListener('mouseup', this.onMouseUp);
75
+ }
76
+
77
+ removeEventListeners() {
78
+ const canvas = this.framework.canvas;
79
+ canvas.removeEventListener('touchstart', this.onTouchStart);
80
+ canvas.removeEventListener('touchmove', this.onTouchMove);
81
+ canvas.removeEventListener('touchend', this.onTouchEnd);
82
+ canvas.removeEventListener('mousedown', this.onMouseDown);
83
+ canvas.removeEventListener('mousemove', this.onMouseMove);
84
+ canvas.removeEventListener('mouseup', this.onMouseUp);
85
+ }
86
+
87
+ // Coordonnées locales
88
+ getLocalPos(clientX, clientY) {
89
+ const rect = this.framework.canvas.getBoundingClientRect();
90
+ const globalX = clientX - rect.left;
91
+ const globalY = clientY - rect.top - this.framework.scrollOffset;
92
+ return {
93
+ x: globalX - this.x,
94
+ y: globalY - this.y
95
+ };
96
+ }
97
+
98
+ handleTouchStart(e) {
99
+ e.preventDefault();
100
+ const touch = e.touches[0];
101
+ const pos = this.getLocalPos(touch.clientX, touch.clientY);
102
+ this.handlePress(pos.x, pos.y);
103
+ }
104
+
105
+ handleTouchMove(e) {
106
+ e.preventDefault();
107
+ }
108
+
109
+ handleTouchEnd(e) {
110
+ e.preventDefault();
111
+ }
112
+
113
+ handleMouseDown(e) {
114
+ const pos = this.getLocalPos(e.clientX, e.clientY);
115
+ this.handlePress(pos.x, pos.y);
116
+ }
117
+
118
+ handleMouseMove(e) {}
119
+
120
+ handleMouseUp(e) {}
121
+
122
+ async startCamera() {
123
+ if (this.stream) return;
124
+
125
+ try {
126
+ this.stopCamera();
127
+
128
+ this.stream = await navigator.mediaDevices.getUserMedia({
129
+ video: {
130
+ facingMode: this.facingMode,
131
+ width: { ideal: 1280 },
132
+ height: { ideal: 720 },
133
+ frameRate: { ideal: 30 }
134
+ }
135
+ });
136
+
137
+ const [track] = this.stream.getVideoTracks();
138
+ const caps = track.getCapabilities?.() || {};
139
+ this.torchSupported = !!caps.torch;
140
+
141
+ this.video = document.createElement('video');
142
+ this.video.autoplay = true;
143
+ this.video.playsInline = true;
144
+ this.video.muted = true;
145
+ this.video.srcObject = this.stream;
146
+
147
+ this.video.style.position = 'fixed';
148
+ this.video.style.left = '-9999px';
149
+ this.video.style.top = '-9999px';
150
+ this.video.style.width = '1px';
151
+ this.video.style.height = '1px';
152
+ document.body.appendChild(this.video);
153
+
154
+ await new Promise(resolve => {
155
+ this.video.onloadedmetadata = () => {
156
+ this.loaded = true;
157
+ this.video.play().catch(e => console.warn('Play auto échoué:', e));
158
+ resolve();
159
+ };
160
+ this.video.onerror = (e) => {
161
+ this.error = 'Erreur vidéo';
162
+ console.error('Video error:', e);
163
+ resolve();
164
+ };
165
+ });
166
+
167
+ this.markDirty();
168
+ } catch (err) {
169
+ this.error = err.message || 'Accès caméra refusé';
170
+ console.error('Échec getUserMedia:', err);
171
+ this.markDirty();
172
+ }
173
+ }
174
+
175
+ stopCamera() {
176
+ if (this.stream) {
177
+ this.stream.getTracks().forEach(track => track.stop());
178
+ this.stream = null;
179
+ }
180
+ if (this.video) {
181
+ if (this.video.parentNode) {
182
+ this.video.parentNode.removeChild(this.video);
183
+ }
184
+ this.video.srcObject = null;
185
+ this.video = null;
186
+ }
187
+ this.loaded = false;
188
+ this.markDirty();
189
+ }
190
+
191
+ async switchCamera() {
192
+ this.stopCamera();
193
+ this.facingMode = this.facingMode === 'user' ? 'environment' : 'user';
194
+ await this.startCamera();
195
+ }
196
+
197
+ async toggleTorch() {
198
+ if (!this.torchSupported || !this.stream) return;
199
+
200
+ const [track] = this.stream.getVideoTracks();
201
+ try {
202
+ await track.applyConstraints({
203
+ advanced: [{ torch: !this.torchOn }]
204
+ });
205
+ this.torchOn = !this.torchOn;
206
+ this.markDirty();
207
+ } catch (err) {
208
+ console.warn('Torch impossible:', err);
209
+ }
210
+ }
211
+
212
+ capturePhoto() {
213
+ if (!this.loaded || !this.video) {
214
+ console.warn('Capture impossible : caméra pas prête');
215
+ return null;
216
+ }
217
+
218
+ const canvas = document.createElement('canvas');
219
+ canvas.width = this.video.videoWidth;
220
+ canvas.height = this.video.videoHeight;
221
+
222
+ const ctx = canvas.getContext('2d');
223
+ ctx.drawImage(this.video, 0, 0);
224
+
225
+ const dataUrl = canvas.toDataURL('image/jpeg', 0.92);
226
+ if (this.onPhoto) this.onPhoto(dataUrl);
227
+ console.log('Photo capturée ! DataURL:', dataUrl.substring(0, 50) + '...');
228
+
229
+ // Feedback : flash + preview 3s
230
+ this.flashTimer = setTimeout(() => {
231
+ this.flashTimer = null;
232
+ this.markDirty();
233
+ }, 200);
234
+
235
+ this.previewPhoto = dataUrl;
236
+ this.previewTimeout = setTimeout(() => {
237
+ this.previewPhoto = null;
238
+ this.markDirty();
239
+ }, 3000);
240
+
241
+ this.markDirty();
242
+ return dataUrl;
243
+ }
244
+
245
+ switchFitMode() {
246
+ this.currentFitModeIndex = (this.currentFitModeIndex + 1) % this.fitModes.length;
247
+ this.fitMode = this.fitModes[this.currentFitModeIndex];
248
+ this.markDirty();
249
+ }
250
+
251
+ handlePress(relX, relY) {
252
+
253
+ // Capture centrale
254
+ const captureX = this.width / 2;
255
+ const captureY = this.height - 60;
256
+ if (Math.hypot(relX - captureX, relY - captureY) < this.captureButtonRadius + 10) {
257
+ this.capturePhoto();
258
+ return;
259
+ }
260
+
261
+ // Switch caméra
262
+ if (relX < 60 && relY < 60) {
263
+ this.switchCamera();
264
+ return;
265
+ }
266
+
267
+ // Torch
268
+ if (this.torchSupported && relX > this.width - 60 && relY < 60) {
269
+ this.toggleTorch();
270
+ return;
271
+ }
272
+
273
+ // Switch mode
274
+ const modeButtonX = this.width - 80;
275
+ const modeButtonY = 20;
276
+ if (relX > modeButtonX && relX < modeButtonX + this.modeButtonSize &&
277
+ relY > modeButtonY && relY < modeButtonY + this.modeButtonSize) {
278
+ this.switchFitMode();
279
+ return;
280
+ }
281
+ }
282
+
283
+ drawContainIcon(ctx, x, y, size) {
284
+ // Icône "contain" : rectangle avec flèches vers l'intérieur
285
+ const pad = size * 0.2;
286
+ ctx.strokeStyle = '#000';
287
+ ctx.lineWidth = 2;
288
+
289
+ // Rectangle extérieur
290
+ ctx.strokeRect(x + pad, y + pad, size - pad * 2, size - pad * 2);
291
+
292
+ // Flèches pointant vers l'intérieur
293
+ const arrowSize = size * 0.15;
294
+ ctx.fillStyle = '#000';
295
+
296
+ // Flèche haut
297
+ ctx.beginPath();
298
+ ctx.moveTo(x + size/2, y + pad - 2);
299
+ ctx.lineTo(x + size/2 - arrowSize, y + pad + arrowSize);
300
+ ctx.lineTo(x + size/2 + arrowSize, y + pad + arrowSize);
301
+ ctx.fill();
302
+
303
+ // Flèche bas
304
+ ctx.beginPath();
305
+ ctx.moveTo(x + size/2, y + size - pad + 2);
306
+ ctx.lineTo(x + size/2 - arrowSize, y + size - pad - arrowSize);
307
+ ctx.lineTo(x + size/2 + arrowSize, y + size - pad - arrowSize);
308
+ ctx.fill();
309
+
310
+ // Flèche gauche
311
+ ctx.beginPath();
312
+ ctx.moveTo(x + pad - 2, y + size/2);
313
+ ctx.lineTo(x + pad + arrowSize, y + size/2 - arrowSize);
314
+ ctx.lineTo(x + pad + arrowSize, y + size/2 + arrowSize);
315
+ ctx.fill();
316
+
317
+ // Flèche droite
318
+ ctx.beginPath();
319
+ ctx.moveTo(x + size - pad + 2, y + size/2);
320
+ ctx.lineTo(x + size - pad - arrowSize, y + size/2 - arrowSize);
321
+ ctx.lineTo(x + size - pad - arrowSize, y + size/2 + arrowSize);
322
+ ctx.fill();
323
+ }
324
+
325
+ drawCoverIcon(ctx, x, y, size) {
326
+ // Icône "cover" : rectangle avec flèches vers l'extérieur
327
+ const pad = size * 0.2;
328
+ ctx.strokeStyle = '#000';
329
+ ctx.lineWidth = 2;
330
+
331
+ // Rectangle intérieur
332
+ ctx.strokeRect(x + pad, y + pad, size - pad * 2, size - pad * 2);
333
+
334
+ // Flèches pointant vers l'extérieur
335
+ const arrowSize = size * 0.15;
336
+ ctx.fillStyle = '#000';
337
+
338
+ // Flèche haut
339
+ ctx.beginPath();
340
+ ctx.moveTo(x + size/2, y + 2);
341
+ ctx.lineTo(x + size/2 - arrowSize, y + arrowSize + 2);
342
+ ctx.lineTo(x + size/2 + arrowSize, y + arrowSize + 2);
343
+ ctx.fill();
344
+
345
+ // Flèche bas
346
+ ctx.beginPath();
347
+ ctx.moveTo(x + size/2, y + size - 2);
348
+ ctx.lineTo(x + size/2 - arrowSize, y + size - arrowSize - 2);
349
+ ctx.lineTo(x + size/2 + arrowSize, y + size - arrowSize - 2);
350
+ ctx.fill();
351
+
352
+ // Flèche gauche
353
+ ctx.beginPath();
354
+ ctx.moveTo(x + 2, y + size/2);
355
+ ctx.lineTo(x + arrowSize + 2, y + size/2 - arrowSize);
356
+ ctx.lineTo(x + arrowSize + 2, y + size/2 + arrowSize);
357
+ ctx.fill();
358
+
359
+ // Flèche droite
360
+ ctx.beginPath();
361
+ ctx.moveTo(x + size - 2, y + size/2);
362
+ ctx.lineTo(x + size - arrowSize - 2, y + size/2 - arrowSize);
363
+ ctx.lineTo(x + size - arrowSize - 2, y + size/2 + arrowSize);
364
+ ctx.fill();
365
+ }
366
+
367
+ drawContainIcon(ctx, x, y, size) {
368
+ // Icône "contain" : rectangle avec flèches vers l'intérieur
369
+ const pad = size * 0.2;
370
+ ctx.strokeStyle = '#000';
371
+ ctx.lineWidth = 2;
372
+
373
+ // Rectangle extérieur
374
+ ctx.strokeRect(x + pad, y + pad, size - pad * 2, size - pad * 2);
375
+
376
+ // Flèches pointant vers l'intérieur
377
+ const arrowSize = size * 0.15;
378
+ ctx.fillStyle = '#000';
379
+
380
+ // Flèche haut
381
+ ctx.beginPath();
382
+ ctx.moveTo(x + size/2, y + pad - 2);
383
+ ctx.lineTo(x + size/2 - arrowSize, y + pad + arrowSize);
384
+ ctx.lineTo(x + size/2 + arrowSize, y + pad + arrowSize);
385
+ ctx.fill();
386
+
387
+ // Flèche bas
388
+ ctx.beginPath();
389
+ ctx.moveTo(x + size/2, y + size - pad + 2);
390
+ ctx.lineTo(x + size/2 - arrowSize, y + size - pad - arrowSize);
391
+ ctx.lineTo(x + size/2 + arrowSize, y + size - pad - arrowSize);
392
+ ctx.fill();
393
+
394
+ // Flèche gauche
395
+ ctx.beginPath();
396
+ ctx.moveTo(x + pad - 2, y + size/2);
397
+ ctx.lineTo(x + pad + arrowSize, y + size/2 - arrowSize);
398
+ ctx.lineTo(x + pad + arrowSize, y + size/2 + arrowSize);
399
+ ctx.fill();
400
+
401
+ // Flèche droite
402
+ ctx.beginPath();
403
+ ctx.moveTo(x + size - pad + 2, y + size/2);
404
+ ctx.lineTo(x + size - pad - arrowSize, y + size/2 - arrowSize);
405
+ ctx.lineTo(x + size - pad - arrowSize, y + size/2 + arrowSize);
406
+ ctx.fill();
407
+ }
408
+
409
+ drawCoverIcon(ctx, x, y, size) {
410
+ // Icône "cover" : rectangle avec flèches vers l'extérieur
411
+ const pad = size * 0.2;
412
+ ctx.strokeStyle = '#000';
413
+ ctx.lineWidth = 2;
414
+
415
+ // Rectangle intérieur
416
+ ctx.strokeRect(x + pad, y + pad, size - pad * 2, size - pad * 2);
417
+
418
+ // Flèches pointant vers l'extérieur
419
+ const arrowSize = size * 0.15;
420
+ ctx.fillStyle = '#000';
421
+
422
+ // Flèche haut
423
+ ctx.beginPath();
424
+ ctx.moveTo(x + size/2, y + 2);
425
+ ctx.lineTo(x + size/2 - arrowSize, y + arrowSize + 2);
426
+ ctx.lineTo(x + size/2 + arrowSize, y + arrowSize + 2);
427
+ ctx.fill();
428
+
429
+ // Flèche bas
430
+ ctx.beginPath();
431
+ ctx.moveTo(x + size/2, y + size - 2);
432
+ ctx.lineTo(x + size/2 - arrowSize, y + size - arrowSize - 2);
433
+ ctx.lineTo(x + size/2 + arrowSize, y + size - arrowSize - 2);
434
+ ctx.fill();
435
+
436
+ // Flèche gauche
437
+ ctx.beginPath();
438
+ ctx.moveTo(x + 2, y + size/2);
439
+ ctx.lineTo(x + arrowSize + 2, y + size/2 - arrowSize);
440
+ ctx.lineTo(x + arrowSize + 2, y + size/2 + arrowSize);
441
+ ctx.fill();
442
+
443
+ // Flèche droite
444
+ ctx.beginPath();
445
+ ctx.moveTo(x + size - 2, y + size/2);
446
+ ctx.lineTo(x + size - arrowSize - 2, y + size/2 - arrowSize);
447
+ ctx.lineTo(x + size - arrowSize - 2, y + size/2 + arrowSize);
448
+ ctx.fill();
449
+ }
450
+
451
+ drawSwitchCameraIcon(ctx, x, y, size) {
452
+ // Icône de switch caméra : deux caméras avec flèche circulaire
453
+ const centerX = x + size / 2;
454
+ const centerY = y + size / 2;
455
+ const radius = size * 0.35;
456
+
457
+ ctx.strokeStyle = '#ffffff';
458
+ ctx.lineWidth = 3;
459
+ ctx.lineCap = 'round';
460
+
461
+ // Arc circulaire (flèche de rotation)
462
+ ctx.beginPath();
463
+ ctx.arc(centerX, centerY, radius, -Math.PI * 0.7, Math.PI * 0.7);
464
+ ctx.stroke();
465
+
466
+ // Flèche en haut à droite
467
+ const arrowSize = size * 0.15;
468
+ const arrowAngle = Math.PI * 0.7;
469
+ const arrowX = centerX + radius * Math.cos(arrowAngle);
470
+ const arrowY = centerY + radius * Math.sin(arrowAngle);
471
+
472
+ ctx.fillStyle = '#ffffff';
473
+ ctx.beginPath();
474
+ ctx.moveTo(arrowX, arrowY);
475
+ ctx.lineTo(arrowX - arrowSize, arrowY - arrowSize * 0.5);
476
+ ctx.lineTo(arrowX - arrowSize * 0.5, arrowY + arrowSize);
477
+ ctx.fill();
478
+
479
+ // Mini caméra au centre
480
+ const camWidth = size * 0.25;
481
+ const camHeight = size * 0.18;
482
+ const camX = centerX - camWidth / 2;
483
+ const camY = centerY - camHeight / 2;
484
+
485
+ ctx.fillStyle = '#ffffff';
486
+ ctx.fillRect(camX, camY, camWidth, camHeight);
487
+
488
+ // Objectif
489
+ ctx.fillStyle = '#000';
490
+ ctx.beginPath();
491
+ ctx.arc(centerX, centerY, size * 0.08, 0, Math.PI * 2);
492
+ ctx.fill();
493
+ }
494
+
495
+ draw(ctx) {
496
+ ctx.save();
497
+
498
+ ctx.fillStyle = '#000';
499
+ ctx.fillRect(this.x, this.y, this.width, this.height);
500
+
501
+ // Flash blanc après capture
502
+ if (this.flashTimer) {
503
+ ctx.fillStyle = 'rgba(255,255,255,0.6)';
504
+ ctx.fillRect(this.x, this.y, this.width, this.height);
505
+ }
506
+
507
+ if (this.error) {
508
+ ctx.fillStyle = '#ff4444';
509
+ ctx.font = '16px Arial';
510
+ ctx.textAlign = 'center';
511
+ ctx.textBaseline = 'middle';
512
+ ctx.fillText(this.error, this.x + this.width/2, this.y + this.height/2);
513
+ } else if (!this.loaded) {
514
+ ctx.fillStyle = '#fff';
515
+ ctx.font = '16px Arial';
516
+ ctx.textAlign = 'center';
517
+ ctx.textBaseline = 'middle';
518
+ ctx.fillText('Démarrage caméra...', this.x + this.width/2, this.y + this.height/2);
519
+ } else if (this.video && this.loaded) {
520
+ const videoRatio = this.video.videoWidth / this.video.videoHeight;
521
+ const canvasRatio = this.width / this.height;
522
+
523
+ let drawWidth = this.width;
524
+ let drawHeight = this.height;
525
+ let offsetX = 0;
526
+ let offsetY = 0;
527
+
528
+ if (this.fitMode === 'cover') {
529
+ if (videoRatio > canvasRatio) {
530
+ drawHeight = this.height;
531
+ drawWidth = drawHeight * videoRatio;
532
+ offsetX = (this.width - drawWidth) / 2;
533
+ } else {
534
+ drawWidth = this.width;
535
+ drawHeight = drawWidth / videoRatio;
536
+ offsetY = (this.height - drawHeight) / 2;
537
+ }
538
+ } else if (this.fitMode === 'contain') {
539
+ if (videoRatio > canvasRatio) {
540
+ drawWidth = this.width;
541
+ drawHeight = drawWidth / videoRatio;
542
+ offsetY = (this.height - drawHeight) / 2;
543
+ } else {
544
+ drawHeight = this.height;
545
+ drawWidth = drawHeight * videoRatio;
546
+ offsetX = (this.width - drawWidth) / 2;
547
+ }
548
+ }
549
+
550
+ ctx.drawImage(this.video, this.x + offsetX, this.y + offsetY, drawWidth, drawHeight);
551
+
552
+ // Mini preview dernière photo (bas droite, 3s)
553
+ if (this.previewPhoto) {
554
+ const previewSize = 80;
555
+ const img = new Image();
556
+ img.src = this.previewPhoto;
557
+ ctx.drawImage(img, this.x + this.width - previewSize - 10, this.y + this.height - previewSize - 10, previewSize, previewSize);
558
+ ctx.strokeStyle = '#fff';
559
+ ctx.lineWidth = 2;
560
+ ctx.strokeRect(this.x + this.width - previewSize - 10, this.y + this.height - previewSize - 10, previewSize, previewSize);
561
+ }
562
+ }
563
+
564
+ // Contrôles bas
565
+ ctx.fillStyle = 'rgba(0,0,0,0.5)';
566
+ ctx.fillRect(this.x, this.y + this.height - 100, this.width, 100);
567
+
568
+ // Bouton capture
569
+ ctx.fillStyle = '#ffffff';
570
+ ctx.beginPath();
571
+ ctx.arc(this.x + this.width/2, this.y + this.height - 50, this.captureButtonRadius, 0, Math.PI * 2);
572
+ ctx.fill();
573
+
574
+ ctx.strokeStyle = '#ff4444';
575
+ ctx.lineWidth = 6;
576
+ ctx.beginPath();
577
+ ctx.arc(this.x + this.width/2, this.y + this.height - 50, this.captureButtonRadius + 10, 0, Math.PI * 2);
578
+ ctx.stroke();
579
+
580
+ // Switch caméra avec icône
581
+ const switchBtnX = this.x + 20;
582
+ const switchBtnY = this.y + 20;
583
+ const switchBtnSize = 50;
584
+
585
+ // Fond semi-transparent
586
+ ctx.fillStyle = 'rgba(0,0,0,0.5)';
587
+ ctx.beginPath();
588
+ ctx.arc(switchBtnX + switchBtnSize/2, switchBtnY + switchBtnSize/2, switchBtnSize/2, 0, Math.PI * 2);
589
+ ctx.fill();
590
+
591
+ // Icône
592
+ this.drawSwitchCameraIcon(ctx, switchBtnX, switchBtnY, switchBtnSize);
593
+
594
+ // Torch
595
+ if (this.torchSupported) {
596
+ ctx.fillStyle = this.torchOn ? '#ffeb3b' : '#ffffff';
597
+ ctx.fillText('⚡', this.x + this.width - 50, this.y + 45);
598
+ }
599
+
600
+ // Bouton switch mode avec icône
601
+ const btnX = this.x + this.width - 80;
602
+ const btnY = this.y + 20;
603
+
604
+ // Fond du bouton
605
+ ctx.fillStyle = 'rgba(255,255,255,0.9)';
606
+ ctx.fillRect(btnX, btnY, this.modeButtonSize, this.modeButtonSize);
607
+
608
+ // Bordure
609
+ ctx.strokeStyle = '#000';
610
+ ctx.lineWidth = 2;
611
+ ctx.strokeRect(btnX, btnY, this.modeButtonSize, this.modeButtonSize);
612
+
613
+ // Dessiner l'icône appropriée
614
+ if (this.fitMode === 'contain') {
615
+ this.drawContainIcon(ctx, btnX, btnY, this.modeButtonSize);
616
+ } else {
617
+ this.drawCoverIcon(ctx, btnX, btnY, this.modeButtonSize);
618
+ }
619
+
620
+ ctx.restore();
621
+ }
622
+ }
623
+
624
+ export default FloatedCamera;
@@ -55,6 +55,7 @@ import Chart from '../components/Chart.js';
55
55
  import SliverAppBar from '../components/SliverAppBar.js';
56
56
  import AudioPlayer from '../components/AudioPlayer.js';
57
57
  import Camera from '../components/Camera.js';
58
+ import FloatedCamera from '../components/FloatedCamera.js';
58
59
  import TimePicker from '../components/TimePicker.js';
59
60
  import QRCodeReader from '../components/QRCodeReader.js';
60
61
 
@@ -1806,7 +1807,7 @@ class CanvasFramework {
1806
1807
  // Hook beforeLeave de la route actuelle (peut bloquer la navigation)
1807
1808
  const currentRouteData = this.routes.get(this.currentRoute);
1808
1809
  if (currentRouteData?.beforeLeave) {
1809
- const canLeave = await currentRouteData.beforeLeave(this.currentParams, this.currentQuery);
1810
+ const canLeave = await currentRouteData.beforeLeave(this.currentParams, this.currentQuery, this);
1810
1811
  if (canLeave === false) {
1811
1812
  console.log('Navigation cancelled by beforeLeave hook');
1812
1813
  return;
@@ -1815,14 +1816,14 @@ class CanvasFramework {
1815
1816
 
1816
1817
  // ✅ NOUVEAU : Hook onLeave (alias plus intuitif de beforeLeave, mais ne bloque pas)
1817
1818
  if (currentRouteData?.onLeave) {
1818
- await currentRouteData.onLeave(this.currentParams, this.currentQuery);
1819
+ await currentRouteData.onLeave(this.currentParams, this.currentQuery, this);
1819
1820
  }
1820
1821
 
1821
1822
  // ===== LIFECYCLE: AVANT D'ENTRER DANS LA NOUVELLE ROUTE =====
1822
1823
 
1823
1824
  // Hook beforeEnter de la nouvelle route (peut bloquer la navigation)
1824
1825
  if (route.beforeEnter) {
1825
- const canEnter = await route.beforeEnter(params, query);
1826
+ const canEnter = await route.beforeEnter(params, query, this);
1826
1827
  if (canEnter === false) {
1827
1828
  console.log('Navigation cancelled by beforeEnter hook');
1828
1829
  return;
@@ -1831,7 +1832,7 @@ class CanvasFramework {
1831
1832
 
1832
1833
  // ✅ NOUVEAU : Hook onEnter (appelé juste avant de créer les composants)
1833
1834
  if (route.onEnter) {
1834
- await route.onEnter(params, query);
1835
+ await route.onEnter(params, query, this);
1835
1836
  }
1836
1837
 
1837
1838
  // ===== SAUVEGARDER L'ÉTAT ACTUEL =====
@@ -1910,7 +1911,7 @@ class CanvasFramework {
1910
1911
 
1911
1912
  // Hook afterEnter (appelé immédiatement après la création des composants)
1912
1913
  if (route.afterEnter) {
1913
- route.afterEnter(params, query);
1914
+ route.afterEnter(params, query, this);
1914
1915
  }
1915
1916
 
1916
1917
  // ✅ NOUVEAU : Hook afterLeave de l'ancienne route (après transition complète)
@@ -1918,11 +1919,11 @@ class CanvasFramework {
1918
1919
  // Si animation, attendre la fin de la transition
1919
1920
  if (animate && this.transitionState.isTransitioning) {
1920
1921
  setTimeout(() => {
1921
- currentRouteData.afterLeave(oldParams, oldQuery);
1922
+ currentRouteData.afterLeave(oldParams, oldQuery, this);
1922
1923
  }, this.transitionState.duration || 300);
1923
1924
  } else {
1924
1925
  // Pas d'animation, appeler immédiatement
1925
- currentRouteData.afterLeave(oldParams, oldQuery);
1926
+ currentRouteData.afterLeave(oldParams, oldQuery, this);
1926
1927
  }
1927
1928
  }
1928
1929
 
package/core/UIBuilder.js CHANGED
@@ -56,6 +56,7 @@ import Chart from '../components/Chart.js';
56
56
  import SliverAppBar from '../components/SliverAppBar.js';
57
57
  import AudioPlayer from '../components/AudioPlayer.js';
58
58
  import Camera from '../components/Camera.js';
59
+ import FloatedCamera from '../components/FloatedCamera.js';
59
60
  import TimePicker from '../components/TimePicker.js';
60
61
  import QRCodeReader from '../components/QRCodeReader.js';
61
62
 
package/index.js CHANGED
@@ -63,6 +63,7 @@ export { default as Chart } from './components/Chart.js';
63
63
  export { default as SliverAppBar } from './components/SliverAppBar.js';
64
64
  export { default as AudioPlayer } from './components/AudioPlayer.js';
65
65
  export { default as Camera } from './components/Camera.js';
66
+ export { default as FloatedCamera } from './components/FloatedCamera.js';
66
67
  export { default as TimePicker } from './components/TimePicker.js';
67
68
  export { default as QRCodeReader } from './components/QRCodeReader.js';
68
69
 
@@ -115,4 +116,4 @@ export { default as FeatureFlags } from './manager/FeatureFlags.js';
115
116
 
116
117
  // Version du framework
117
118
 
118
- export const VERSION = '0.4.7';
119
+ export const VERSION = '0.5.7';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasframework",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "Canvas-based cross-platform UI framework (Material & Cupertino)",
5
5
  "type": "module",
6
6
  "main": "./index.js",