canvasframework 0.3.25 → 0.3.27

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 Camera autonome avec gestion directe des clics/touches
5
+ * Modes : contain (tout visible + bandes), cover (remplit + crop), fit (centre sans crop)
6
+ */
7
+ class Camera 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 Camera;
@@ -54,6 +54,7 @@ import Banner from '../components/Banner.js';
54
54
  import Chart from '../components/Chart.js';
55
55
  import SliverAppBar from '../components/SliverAppBar.js';
56
56
  import AudioPlayer from '../components/AudioPlayer.js';
57
+ import Camera from '../components/Camera.js';
57
58
 
58
59
  // Utils
59
60
  import SafeArea from '../utils/SafeArea.js';
@@ -134,6 +135,7 @@ const FIXED_COMPONENT_TYPES = new Set([
134
135
  FAB,
135
136
  Toast,
136
137
  Banner,
138
+ Camera,
137
139
  SliverAppBar,
138
140
  BottomSheet,
139
141
  ContextMenu,
package/core/UIBuilder.js CHANGED
@@ -55,6 +55,7 @@ import Banner from '../components/Banner.js';
55
55
  import Chart from '../components/Chart.js';
56
56
  import SliverAppBar from '../components/SliverAppBar.js';
57
57
  import AudioPlayer from '../components/AudioPlayer.js';
58
+ import Camera from '../components/Camera.js';
58
59
 
59
60
  // Features
60
61
  import PullToRefresh from '../features/PullToRefresh.js';
@@ -96,6 +97,7 @@ const Components = {
96
97
  AppBar,
97
98
  SliverAppBar,
98
99
  Chip,
100
+ Camera,
99
101
  Stepper,
100
102
  Accordion,
101
103
  Tabs,
package/index.js CHANGED
@@ -62,6 +62,7 @@ export { default as Banner } from './components/Banner.js';
62
62
  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
+ export { default as Camera } from './components/Camera.js';
65
66
 
66
67
  // Utils
67
68
  export { default as SafeArea } from './utils/SafeArea.js';
@@ -104,7 +105,7 @@ export { default as FeatureFlags } from './manager/FeatureFlags.js';
104
105
 
105
106
  // Version du framework
106
107
 
107
- export const VERSION = '0.3.24';
108
+ export const VERSION = '0.3.23';
108
109
 
109
110
 
110
111
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasframework",
3
- "version": "0.3.25",
3
+ "version": "0.3.27",
4
4
  "description": "Canvas-based cross-platform UI framework (Material & Cupertino)",
5
5
  "type": "module",
6
6
  "main": "./index.js",