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.
- package/components/FloatedCamera.js +624 -0
- package/core/CanvasFramework.js +8 -7
- package/core/UIBuilder.js +1 -0
- package/index.js +2 -1
- package/package.json +1 -1
|
@@ -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;
|
package/core/CanvasFramework.js
CHANGED
|
@@ -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.
|
|
119
|
+
export const VERSION = '0.5.7';
|