canvasframework 0.3.22 → 0.3.24

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,611 @@
1
+ import Component from '../core/Component.js';
2
+
3
+ /**
4
+ * Lecteur audio avec gestion directe des événements
5
+ * @class
6
+ * @extends Component
7
+ */
8
+ class AudioPlayer extends Component {
9
+ constructor(framework, options = {}) {
10
+ super(framework, options);
11
+ this.src = options.src || '';
12
+ this.playing = false;
13
+ this.platform = framework.platform;
14
+ this.showControls = true;
15
+ this.controlsTimeout = null;
16
+ this.currentTime = 0;
17
+ this.duration = 0;
18
+ this.progress = 0;
19
+ this.volume = 1;
20
+ this.showVolume = false;
21
+ this.loaded = false;
22
+ this.isLoading = false;
23
+ this.userInteracted = false;
24
+
25
+ // Élément audio HTML caché
26
+ this.audioElement = null;
27
+
28
+ // Gestion directe des événements
29
+ this.canvas = framework.canvas;
30
+ this.isMouseDownOnPlayer = false;
31
+ this.clickStartTime = 0;
32
+ this.clickThreshold = 200; // ms
33
+
34
+ this.controlsHeight = 50;
35
+
36
+ // Callbacks
37
+ this.onPlay = options.onPlay;
38
+ this.onPause = options.onPause;
39
+ this.onEnded = options.onEnded;
40
+
41
+ // NE PAS utiliser les handlers du framework
42
+ this.onPress = null;
43
+ this.onMove = null;
44
+ this.onRelease = null;
45
+
46
+ // Attacher les événements directement au canvas
47
+ this.setupDirectEventListeners();
48
+
49
+ // Initialiser l'audio
50
+ this.initAudio();
51
+ }
52
+
53
+ setupDirectEventListeners() {
54
+ // Stocker les listeners originaux pour pouvoir les retirer plus tard
55
+ this.originalListeners = {
56
+ mousedown: this.canvas.onmousedown,
57
+ mouseup: this.canvas.onmouseup,
58
+ mousemove: this.canvas.onmousemove,
59
+ touchstart: this.canvas.ontouchstart,
60
+ touchend: this.canvas.ontouchend,
61
+ touchmove: this.canvas.ontouchmove
62
+ };
63
+
64
+ // Attacher nos propres handlers
65
+ this.canvas.addEventListener('mousedown', this.handleCanvasMouseDown.bind(this));
66
+ this.canvas.addEventListener('mouseup', this.handleCanvasMouseUp.bind(this));
67
+ this.canvas.addEventListener('mousemove', this.handleCanvasMouseMove.bind(this));
68
+
69
+ this.canvas.addEventListener('touchstart', this.handleCanvasTouchStart.bind(this));
70
+ this.canvas.addEventListener('touchend', this.handleCanvasTouchEnd.bind(this));
71
+ this.canvas.addEventListener('touchmove', this.handleCanvasTouchMove.bind(this));
72
+ }
73
+
74
+ removeDirectEventListeners() {
75
+ // Retirer nos listeners
76
+ this.canvas.removeEventListener('mousedown', this.handleCanvasMouseDown);
77
+ this.canvas.removeEventListener('mouseup', this.handleCanvasMouseUp);
78
+ this.canvas.removeEventListener('mousemove', this.handleCanvasMouseMove);
79
+
80
+ this.canvas.removeEventListener('touchstart', this.handleCanvasTouchStart);
81
+ this.canvas.removeEventListener('touchend', this.handleCanvasTouchEnd);
82
+ this.canvas.removeEventListener('touchmove', this.handleCanvasTouchMove);
83
+
84
+ // Restaurer les listeners originaux
85
+ Object.keys(this.originalListeners).forEach(event => {
86
+ if (this.originalListeners[event]) {
87
+ this.canvas[`on${event}`] = this.originalListeners[event];
88
+ }
89
+ });
90
+ }
91
+
92
+ handleCanvasMouseDown(e) {
93
+ const rect = this.canvas.getBoundingClientRect();
94
+ const x = e.clientX - rect.left;
95
+ const y = e.clientY - rect.top;
96
+
97
+ if (this.isPointInside(x, y)) {
98
+ e.stopPropagation();
99
+ this.isMouseDownOnPlayer = true;
100
+ this.clickStartTime = Date.now();
101
+ this.handleInteraction(x, y, 'start');
102
+ return true; // Empêche la propagation au framework
103
+ }
104
+ }
105
+
106
+ handleCanvasMouseUp(e) {
107
+ if (!this.isMouseDownOnPlayer) return;
108
+
109
+ const rect = this.canvas.getBoundingClientRect();
110
+ const x = e.clientX - rect.left;
111
+ const y = e.clientY - rect.top;
112
+
113
+ const clickDuration = Date.now() - this.clickStartTime;
114
+
115
+ if (clickDuration < this.clickThreshold && this.isPointInside(x, y)) {
116
+ e.stopPropagation();
117
+ this.handleInteraction(x, y, 'end');
118
+ }
119
+
120
+ this.isMouseDownOnPlayer = false;
121
+ return true; // Empêche la propagation au framework
122
+ }
123
+
124
+ handleCanvasMouseMove(e) {
125
+ if (!this.isMouseDownOnPlayer) return;
126
+
127
+ const rect = this.canvas.getBoundingClientRect();
128
+ const x = e.clientX - rect.left;
129
+ const y = e.clientY - rect.top;
130
+
131
+ if (this.isPointInside(x, y)) {
132
+ e.stopPropagation();
133
+ this.handleInteraction(x, y, 'move');
134
+ return true;
135
+ }
136
+ }
137
+
138
+ handleCanvasTouchStart(e) {
139
+ const touch = e.touches[0];
140
+ const rect = this.canvas.getBoundingClientRect();
141
+ const x = touch.clientX - rect.left;
142
+ const y = touch.clientY - rect.top;
143
+
144
+ if (this.isPointInside(x, y)) {
145
+ e.stopPropagation();
146
+ e.preventDefault();
147
+ this.isMouseDownOnPlayer = true;
148
+ this.clickStartTime = Date.now();
149
+ this.handleInteraction(x, y, 'start');
150
+ return true;
151
+ }
152
+ }
153
+
154
+ handleCanvasTouchEnd(e) {
155
+ if (!this.isMouseDownOnPlayer) return;
156
+
157
+ const touch = e.changedTouches[0];
158
+ const rect = this.canvas.getBoundingClientRect();
159
+ const x = touch.clientX - rect.left;
160
+ const y = touch.clientY - rect.top;
161
+
162
+ const clickDuration = Date.now() - this.clickStartTime;
163
+
164
+ if (clickDuration < this.clickThreshold && this.isPointInside(x, y)) {
165
+ e.stopPropagation();
166
+ e.preventDefault();
167
+ this.handleInteraction(x, y, 'end');
168
+ }
169
+
170
+ this.isMouseDownOnPlayer = false;
171
+ return true;
172
+ }
173
+
174
+ handleCanvasTouchMove(e) {
175
+ if (!this.isMouseDownOnPlayer) return;
176
+
177
+ const touch = e.touches[0];
178
+ const rect = this.canvas.getBoundingClientRect();
179
+ const x = touch.clientX - rect.left;
180
+ const y = touch.clientY - rect.top;
181
+
182
+ if (this.isPointInside(x, y)) {
183
+ e.stopPropagation();
184
+ e.preventDefault();
185
+ this.handleInteraction(x, y, 'move');
186
+ return true;
187
+ }
188
+ }
189
+
190
+ handleInteraction(x, y, type) {
191
+ // Ajuster les coordonnées pour le scrolling
192
+ const adjustedY = y - this.framework.scrollOffset;
193
+
194
+ // Afficher les contrôles
195
+ this.showControls = true;
196
+ this.showControlsTemporarily();
197
+
198
+ // Marquer l'interaction utilisateur
199
+ if (!this.userInteracted) {
200
+ this.userInteracted = true;
201
+ this.showInteractionMessage = false;
202
+ }
203
+
204
+ // Calculer la position relative
205
+ const localX = x - this.x;
206
+ const localY = adjustedY - this.y;
207
+
208
+ // Bouton play/pause central
209
+ const centerX = this.width / 2;
210
+ const centerY = this.height / 2;
211
+
212
+ const buttonSize = 40;
213
+ const isInButton = localX >= centerX - buttonSize && localX <= centerX + buttonSize &&
214
+ localY >= centerY - buttonSize && localY <= centerY + buttonSize;
215
+
216
+ // Barre de progression
217
+ const progressBarY = this.height - 30;
218
+ const isInProgressBar = this.showControls && this.loaded &&
219
+ localY >= progressBarY && localY <= progressBarY + 15;
220
+
221
+ // Traiter selon le type d'événement
222
+ switch (type) {
223
+ case 'start':
224
+ // Marquer le début du clic
225
+ break;
226
+
227
+ case 'move':
228
+ if (isInProgressBar) {
229
+ const newProgress = Math.max(0, Math.min(100, (localX / this.width) * 100));
230
+ this.seekTo((this.duration * newProgress) / 100);
231
+ }
232
+ break;
233
+
234
+ case 'end':
235
+ // Bouton play/pause
236
+ if (isInButton) {
237
+ console.log('AudioPlayer: Bouton cliqué (gestion directe)');
238
+ if (!this.loaded && !this.isLoading) {
239
+ this.initAudio();
240
+ } else if (this.loaded) {
241
+ this.togglePlay();
242
+ }
243
+ }
244
+ break;
245
+ }
246
+ }
247
+
248
+ initAudio() {
249
+ if (!this.src) {
250
+ console.warn('AudioPlayer: Pas de source audio fournie');
251
+ return;
252
+ }
253
+
254
+ console.log('AudioPlayer: Initialisation de l\'audio:', this.src);
255
+
256
+ this.isLoading = true;
257
+
258
+ try {
259
+ // Créer l'élément audio
260
+ this.audioElement = document.createElement('audio');
261
+ this.audioElement.src = this.src;
262
+ this.audioElement.preload = 'auto';
263
+ this.audioElement.crossOrigin = 'anonymous';
264
+ this.audioElement.style.display = 'none';
265
+ this.audioElement.style.position = 'absolute';
266
+ this.audioElement.style.left = '-9999px';
267
+
268
+ this.audioElement.autoplay = false;
269
+ this.audioElement.controls = false;
270
+
271
+ document.body.appendChild(this.audioElement);
272
+
273
+ // Événements de l'audio
274
+ this.audioElement.addEventListener('loadedmetadata', () => {
275
+ console.log('AudioPlayer: Métadonnées chargées');
276
+ this.duration = this.audioElement.duration;
277
+ this.loaded = true;
278
+ this.isLoading = false;
279
+ });
280
+
281
+ this.audioElement.addEventListener('timeupdate', () => {
282
+ this.currentTime = this.audioElement.currentTime;
283
+ this.progress = (this.currentTime / this.duration) * 100;
284
+ });
285
+
286
+ this.audioElement.addEventListener('ended', () => {
287
+ console.log('AudioPlayer: Audio terminé');
288
+ this.playing = false;
289
+ if (this.onEnded) this.onEnded();
290
+ });
291
+
292
+ this.audioElement.addEventListener('play', () => {
293
+ console.log('AudioPlayer: Lecture démarrée');
294
+ this.playing = true;
295
+ if (this.onPlay) this.onPlay();
296
+ });
297
+
298
+ this.audioElement.addEventListener('pause', () => {
299
+ console.log('AudioPlayer: Lecture en pause');
300
+ this.playing = false;
301
+ if (this.onPause) this.onPause();
302
+ });
303
+
304
+ this.audioElement.addEventListener('error', (e) => {
305
+ console.error('AudioPlayer: Erreur audio:', e);
306
+ this.isLoading = false;
307
+ this.loaded = false;
308
+ });
309
+
310
+ this.audioElement.addEventListener('canplaythrough', () => {
311
+ console.log('AudioPlayer: Prêt à jouer');
312
+ this.loaded = true;
313
+ this.isLoading = false;
314
+ });
315
+
316
+ // Démarrer le chargement
317
+ this.audioElement.load();
318
+
319
+ } catch (error) {
320
+ console.error('AudioPlayer: Erreur d\'initialisation:', error);
321
+ this.isLoading = false;
322
+ this.loaded = false;
323
+ }
324
+ }
325
+
326
+ play() {
327
+ if (!this.loaded || !this.audioElement) {
328
+ console.log('AudioPlayer: Impossible de jouer - pas chargé');
329
+ return;
330
+ }
331
+
332
+ // Vérifier l'interaction utilisateur
333
+ if (!this.userInteracted) {
334
+ console.log('AudioPlayer: Attente interaction utilisateur...');
335
+ this.showInteractionMessage = true;
336
+ setTimeout(() => {
337
+ this.showInteractionMessage = false;
338
+ }, 2000);
339
+ return;
340
+ }
341
+
342
+ console.log('AudioPlayer: Lancement de la lecture');
343
+
344
+ const playPromise = this.audioElement.play();
345
+
346
+ if (playPromise !== undefined) {
347
+ playPromise
348
+ .then(() => {
349
+ console.log('AudioPlayer: Lecture réussie');
350
+ this.playing = true;
351
+ this.showControlsTemporarily();
352
+ })
353
+ .catch(error => {
354
+ console.error('AudioPlayer: Erreur de lecture:', error);
355
+ this.playing = false;
356
+
357
+ if (error.name === 'NotAllowedError') {
358
+ console.log('AudioPlayer: Interaction nécessaire');
359
+ this.userInteracted = false;
360
+ }
361
+ });
362
+ }
363
+ }
364
+
365
+ pause() {
366
+ if (!this.playing || !this.audioElement) return;
367
+
368
+ console.log('AudioPlayer: Mise en pause');
369
+ this.audioElement.pause();
370
+ this.playing = false;
371
+ this.showControlsTemporarily();
372
+ }
373
+
374
+ stop() {
375
+ if (this.audioElement) {
376
+ this.audioElement.pause();
377
+ this.audioElement.currentTime = 0;
378
+ }
379
+ this.playing = false;
380
+ this.currentTime = 0;
381
+ this.progress = 0;
382
+ }
383
+
384
+ togglePlay() {
385
+ console.log('AudioPlayer: togglePlay - état actuel:', this.playing);
386
+
387
+ if (!this.loaded) {
388
+ console.log('AudioPlayer: Pas encore chargé');
389
+ if (!this.isLoading) {
390
+ this.initAudio();
391
+ }
392
+ return;
393
+ }
394
+
395
+ if (!this.userInteracted) {
396
+ console.log('AudioPlayer: Interaction utilisateur déclenchée');
397
+ this.userInteracted = true;
398
+ this.showInteractionMessage = false;
399
+ }
400
+
401
+ if (this.playing) {
402
+ this.pause();
403
+ } else {
404
+ this.play();
405
+ }
406
+ }
407
+
408
+ seekTo(time) {
409
+ if (!this.loaded || !this.audioElement) return;
410
+
411
+ const wasPlaying = this.playing;
412
+
413
+ if (wasPlaying) {
414
+ this.audioElement.pause();
415
+ }
416
+
417
+ const newTime = Math.max(0, Math.min(time, this.duration));
418
+ this.audioElement.currentTime = newTime;
419
+ this.currentTime = newTime;
420
+ this.progress = (this.currentTime / this.duration) * 100;
421
+
422
+ if (wasPlaying) {
423
+ this.audioElement.play().catch(e => {
424
+ console.error('AudioPlayer: Erreur reprise après seek:', e);
425
+ });
426
+ }
427
+ }
428
+
429
+ setVolume(value) {
430
+ this.volume = Math.max(0, Math.min(1, value));
431
+ if (this.audioElement) {
432
+ this.audioElement.volume = this.volume;
433
+ }
434
+ }
435
+
436
+ showControlsTemporarily() {
437
+ this.showControls = true;
438
+ clearTimeout(this.controlsTimeout);
439
+ this.controlsTimeout = setTimeout(() => {
440
+ if (this.playing) this.showControls = false;
441
+ }, 3000);
442
+ }
443
+
444
+ update(deltaTime) {
445
+ // Mise à jour automatique via timeupdate
446
+ }
447
+
448
+ draw(ctx) {
449
+ ctx.save();
450
+
451
+ // Fond du lecteur
452
+ ctx.fillStyle = '#111111';
453
+ ctx.fillRect(this.x, this.y, this.width, this.height);
454
+
455
+ // Message d'interaction
456
+ if (this.showInteractionMessage) {
457
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
458
+ ctx.font = 'bold 14px Arial';
459
+ ctx.textAlign = 'center';
460
+ ctx.textBaseline = 'middle';
461
+ ctx.fillText('Cliquez pour activer l\'audio', this.x + this.width / 2, this.y + this.height / 2);
462
+ ctx.restore();
463
+ return;
464
+ }
465
+
466
+ // Indicateur de chargement
467
+ if (this.isLoading) {
468
+ ctx.fillStyle = '#FFFFFF';
469
+ ctx.font = '14px Arial';
470
+ ctx.textAlign = 'center';
471
+ ctx.textBaseline = 'middle';
472
+ ctx.fillText('Chargement...', this.x + this.width / 2, this.y + this.height / 2);
473
+ ctx.restore();
474
+ return;
475
+ }
476
+
477
+ // Indicateur si non chargé
478
+ if (!this.loaded && !this.isLoading) {
479
+ ctx.fillStyle = '#FFFFFF';
480
+ ctx.font = '14px Arial';
481
+ ctx.textAlign = 'center';
482
+ ctx.textBaseline = 'middle';
483
+ ctx.fillText('Audio non disponible', this.x + this.width / 2, this.y + this.height / 2);
484
+ ctx.restore();
485
+ return;
486
+ }
487
+
488
+ // Bouton play/pause
489
+ const centerX = this.x + this.width / 2;
490
+ const centerY = this.y + this.height / 2;
491
+
492
+ if (!this.playing || this.showControls) {
493
+ // Cercle de fond
494
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.6)';
495
+ ctx.beginPath();
496
+ ctx.arc(centerX, centerY, 40, 0, Math.PI * 2);
497
+ ctx.fill();
498
+
499
+ // Icône
500
+ ctx.fillStyle = '#FFFFFF';
501
+ if (this.playing) {
502
+ // Icône pause
503
+ const barWidth = 8;
504
+ const barHeight = 30;
505
+ const gap = 5;
506
+
507
+ ctx.fillRect(centerX - barWidth - gap/2, centerY - barHeight/2, barWidth, barHeight);
508
+ ctx.fillRect(centerX + gap/2, centerY - barHeight/2, barWidth, barHeight);
509
+ } else {
510
+ // Icône play
511
+ const triangleSize = 25;
512
+ ctx.beginPath();
513
+ ctx.moveTo(centerX - triangleSize/2, centerY - triangleSize);
514
+ ctx.lineTo(centerX - triangleSize/2, centerY + triangleSize);
515
+ ctx.lineTo(centerX + triangleSize, centerY);
516
+ ctx.closePath();
517
+ ctx.fill();
518
+ }
519
+ }
520
+
521
+ // Contrôles en bas
522
+ if (this.showControls && this.loaded) {
523
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.8)';
524
+ ctx.fillRect(this.x, this.y + this.height - this.controlsHeight, this.width, this.controlsHeight);
525
+
526
+ const progressX = this.x + 15;
527
+ const progressY = this.y + this.height - 25;
528
+ const progressWidth = this.width - 30;
529
+
530
+ // Barre de progression
531
+ ctx.fillStyle = '#555555';
532
+ ctx.fillRect(progressX, progressY, progressWidth, 6);
533
+ ctx.fillStyle = '#333333';
534
+ ctx.fillRect(progressX, progressY, progressWidth, 2);
535
+
536
+ // Progression actuelle
537
+ ctx.fillStyle = '#FF4444';
538
+ const currentProgressWidth = (progressWidth * this.progress) / 100;
539
+ ctx.fillRect(progressX, progressY, currentProgressWidth, 6);
540
+ ctx.fillStyle = '#FF0000';
541
+ ctx.fillRect(progressX, progressY, currentProgressWidth, 2);
542
+
543
+ // Curseur
544
+ if (currentProgressWidth > 0) {
545
+ const thumbX = progressX + currentProgressWidth;
546
+ ctx.fillStyle = '#FFFFFF';
547
+ ctx.beginPath();
548
+ ctx.arc(thumbX, progressY + 3, 8, 0, Math.PI * 2);
549
+ ctx.fill();
550
+
551
+ ctx.strokeStyle = '#000000';
552
+ ctx.lineWidth = 2;
553
+ ctx.stroke();
554
+ }
555
+
556
+ // Temps
557
+ const currentTimeStr = this.formatTime(this.currentTime);
558
+ const totalTimeStr = this.formatTime(this.duration);
559
+
560
+ ctx.fillStyle = '#FFFFFF';
561
+ ctx.font = 'bold 12px Arial';
562
+ ctx.textAlign = 'left';
563
+ ctx.fillText(currentTimeStr, this.x + 15, this.y + this.height - 35);
564
+
565
+ ctx.textAlign = 'right';
566
+ ctx.fillText(totalTimeStr, this.x + this.width - 15, this.y + this.height - 35);
567
+ }
568
+
569
+ ctx.restore();
570
+ }
571
+
572
+ formatTime(seconds) {
573
+ if (isNaN(seconds) || seconds === Infinity) {
574
+ return "0:00";
575
+ }
576
+
577
+ const mins = Math.floor(seconds / 60);
578
+ const secs = Math.floor(seconds % 60);
579
+ return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
580
+ }
581
+
582
+ isPointInside(x, y) {
583
+ const adjustedY = y - this.framework.scrollOffset;
584
+ return x >= this.x && x <= this.x + this.width &&
585
+ adjustedY >= this.y && adjustedY <= this.y + this.height;
586
+ }
587
+
588
+ remove() {
589
+ // Arrêter la lecture
590
+ this.stop();
591
+
592
+ // Retirer les event listeners
593
+ this.removeDirectEventListeners();
594
+
595
+ // Supprimer l'élément audio
596
+ if (this.audioElement && this.audioElement.parentNode) {
597
+ this.audioElement.pause();
598
+ this.audioElement.src = '';
599
+ this.audioElement.load();
600
+ this.audioElement.parentNode.removeChild(this.audioElement);
601
+ this.audioElement = null;
602
+ }
603
+
604
+ // Nettoyer le timeout
605
+ clearTimeout(this.controlsTimeout);
606
+
607
+ console.log('AudioPlayer: Nettoyé et supprimé');
608
+ }
609
+ }
610
+
611
+ export default AudioPlayer;