canvasframework 0.5.18 → 0.5.20

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.
Files changed (113) hide show
  1. package/README.md +30 -0
  2. package/components/Accordion.js +265 -0
  3. package/components/AndroidDatePickerDialog.js +406 -0
  4. package/components/AppBar.js +398 -0
  5. package/components/AudioPlayer.js +611 -0
  6. package/components/Avatar.js +202 -0
  7. package/components/Banner.js +342 -0
  8. package/components/BottomNavigationBar.js +433 -0
  9. package/components/BottomSheet.js +234 -0
  10. package/components/Button.js +358 -0
  11. package/components/Camera.js +644 -0
  12. package/components/Card.js +193 -0
  13. package/components/Chart.js +700 -0
  14. package/components/Checkbox.js +166 -0
  15. package/components/Chip.js +212 -0
  16. package/components/CircularProgress.js +327 -0
  17. package/components/ContextMenu.js +116 -0
  18. package/components/DatePicker.js +298 -0
  19. package/components/Dialog.js +337 -0
  20. package/components/Divider.js +125 -0
  21. package/components/Drawer.js +276 -0
  22. package/components/FAB.js +270 -0
  23. package/components/FileUpload.js +315 -0
  24. package/components/FloatedCamera.js +644 -0
  25. package/components/IOSDatePickerWheel.js +430 -0
  26. package/components/ImageCarousel.js +219 -0
  27. package/components/ImageComponent.js +223 -0
  28. package/components/Input.js +831 -0
  29. package/components/InputDatalist.js +723 -0
  30. package/components/InputTags.js +624 -0
  31. package/components/List.js +95 -0
  32. package/components/ListItem.js +269 -0
  33. package/components/Modal.js +364 -0
  34. package/components/MorphingFAB.js +428 -0
  35. package/components/MultiSelectDialog.js +206 -0
  36. package/components/NumberInput.js +271 -0
  37. package/components/PasswordInput.js +462 -0
  38. package/components/ProgressBar.js +88 -0
  39. package/components/QRCodeReader.js +539 -0
  40. package/components/RadioButton.js +151 -0
  41. package/components/SearchInput.js +315 -0
  42. package/components/SegmentedControl.js +357 -0
  43. package/components/Select.js +199 -0
  44. package/components/SelectDialog.js +255 -0
  45. package/components/Slider.js +113 -0
  46. package/components/SliverAppBar.js +139 -0
  47. package/components/Snackbar.js +243 -0
  48. package/components/SpeedDialFAB.js +397 -0
  49. package/components/Stepper.js +281 -0
  50. package/components/SwipeableListItem.js +327 -0
  51. package/components/Switch.js +147 -0
  52. package/components/Table.js +492 -0
  53. package/components/Tabs.js +423 -0
  54. package/components/Text.js +141 -0
  55. package/components/TextField.js +151 -0
  56. package/components/TimePicker.js +934 -0
  57. package/components/Toast.js +236 -0
  58. package/components/TreeView.js +420 -0
  59. package/components/Video.js +397 -0
  60. package/components/View.js +140 -0
  61. package/components/VirtualList.js +120 -0
  62. package/core/CanvasFramework.js +3045 -0
  63. package/core/Component.js +243 -0
  64. package/core/ThemeManager.js +358 -0
  65. package/core/UIBuilder.js +267 -0
  66. package/core/WebGLCanvasAdapter.js +782 -0
  67. package/features/Column.js +43 -0
  68. package/features/Grid.js +47 -0
  69. package/features/LayoutComponent.js +43 -0
  70. package/features/OpenStreetMap.js +310 -0
  71. package/features/Positioned.js +33 -0
  72. package/features/PullToRefresh.js +328 -0
  73. package/features/Row.js +40 -0
  74. package/features/SignaturePad.js +257 -0
  75. package/features/Skeleton.js +193 -0
  76. package/features/Stack.js +21 -0
  77. package/index.js +119 -0
  78. package/manager/AccessibilityManager.js +107 -0
  79. package/manager/ErrorHandler.js +59 -0
  80. package/manager/FeatureFlags.js +60 -0
  81. package/manager/MemoryManager.js +107 -0
  82. package/manager/PerformanceMonitor.js +84 -0
  83. package/manager/SecurityManager.js +54 -0
  84. package/package.json +22 -16
  85. package/utils/AnimationEngine.js +734 -0
  86. package/utils/CryptoManager.js +303 -0
  87. package/utils/DataStore.js +403 -0
  88. package/utils/DevTools.js +1618 -0
  89. package/utils/DevToolsConsole.js +201 -0
  90. package/utils/EventBus.js +407 -0
  91. package/utils/FetchClient.js +74 -0
  92. package/utils/FirebaseAuth.js +653 -0
  93. package/utils/FirebaseCore.js +246 -0
  94. package/utils/FirebaseFirestore.js +581 -0
  95. package/utils/FirebaseFunctions.js +97 -0
  96. package/utils/FirebaseRealtimeDB.js +498 -0
  97. package/utils/FirebaseStorage.js +612 -0
  98. package/utils/FormValidator.js +355 -0
  99. package/utils/GeoLocationService.js +62 -0
  100. package/utils/I18n.js +207 -0
  101. package/utils/IndexedDBManager.js +273 -0
  102. package/utils/InspectionOverlay.js +308 -0
  103. package/utils/NotificationManager.js +60 -0
  104. package/utils/OfflineSyncManager.js +342 -0
  105. package/utils/PayPalPayment.js +678 -0
  106. package/utils/QueryBuilder.js +478 -0
  107. package/utils/SafeArea.js +64 -0
  108. package/utils/SecureStorage.js +289 -0
  109. package/utils/StateManager.js +207 -0
  110. package/utils/StripePayment.js +552 -0
  111. package/utils/WebSocketClient.js +66 -0
  112. package/dist/canvasframework.js +0 -2
  113. package/dist/canvasframework.js.LICENSE.txt +0 -1
@@ -0,0 +1,430 @@
1
+ import Component from '../core/Component.js';
2
+
3
+ /**
4
+ * Sélecteur de date iOS (style roue)
5
+ * @class
6
+ * @extends Component
7
+ * @property {Date} selectedDate - Date sélectionnée
8
+ * @property {Function} onChange - Callback au changement
9
+ * @property {number} monthWheel - Mois sélectionné
10
+ * @property {number} dayWheel - Jour sélectionné
11
+ * @property {number} yearWheel - Année sélectionnée
12
+ * @property {number} wheelHeight - Hauteur de la roue
13
+ * @property {number} itemHeight - Hauteur d'un item
14
+ * @property {number} visibleItems - Nombre d'items visibles
15
+ */
16
+ class IOSDatePickerWheel extends Component {
17
+ /**
18
+ * Crée une instance de IOSDatePickerWheel
19
+ * @param {CanvasFramework} framework - Framework parent
20
+ * @param {Object} [options={}] - Options de configuration
21
+ * @param {Date} [options.selectedDate=new Date()] - Date initiale
22
+ * @param {Function} [options.onChange] - Callback au changement
23
+ */
24
+ constructor(framework, options = {}) {
25
+ super(framework, options);
26
+ this.selectedDate = options.selectedDate || new Date();
27
+ this.onChange = options.onChange;
28
+
29
+ // Roues de sélection
30
+ this.monthWheel = this.selectedDate.getMonth();
31
+ this.dayWheel = this.selectedDate.getDate();
32
+ this.yearWheel = this.selectedDate.getFullYear();
33
+
34
+ this.wheelHeight = 200;
35
+ this.itemHeight = 40;
36
+ this.visibleItems = 5;
37
+
38
+ // État interne
39
+ this.dragging = false;
40
+ this.dragStartY = 0;
41
+ this.dragWheel = null;
42
+ this.lastY = 0;
43
+
44
+ // Configuration des limites
45
+ this._setupLimits();
46
+
47
+ // Setup des handlers
48
+ this._setupEventHandlers();
49
+
50
+ // Dimensions
51
+ if (!options.width) {
52
+ this.width = framework.width - 40;
53
+ }
54
+
55
+ if (!options.height) {
56
+ this.height = this.wheelHeight;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Configure les limites pour chaque roue
62
+ * @private
63
+ */
64
+ _setupLimits() {
65
+ // Mois: 0-11 (Janvier à Décembre)
66
+ this.monthMin = 0;
67
+ this.monthMax = 11;
68
+
69
+ // Jour: 1-31 (selon le mois et l'année, on ajustera dynamiquement)
70
+ this.dayMin = 1;
71
+ this.dayMax = 31;
72
+
73
+ // Année: 1900-2100 par défaut
74
+ this.yearMin = 1900;
75
+ this.yearMax = 2100;
76
+
77
+ // Mettre à jour les limites du jour en fonction du mois et de l'année
78
+ this._updateDayLimits();
79
+ }
80
+
81
+ /**
82
+ * Met à jour les limites du jour en fonction du mois et de l'année
83
+ * @private
84
+ */
85
+ _updateDayLimits() {
86
+ // Nombre de jours dans le mois actuel
87
+ const daysInMonth = new Date(this.yearWheel, this.monthWheel + 1, 0).getDate();
88
+ this.dayMax = daysInMonth;
89
+
90
+ // Ajuster le jour sélectionné si nécessaire
91
+ if (this.dayWheel > daysInMonth) {
92
+ this.dayWheel = daysInMonth;
93
+ this._updateSelectedDate();
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Configure les gestionnaires d'événements
99
+ * @private
100
+ */
101
+ _setupEventHandlers() {
102
+ // Handler press
103
+ this.onPress = (x, y) => {
104
+ // Vérifier si dans le composant
105
+ const inside = (x >= this.x && x <= this.x + this.width &&
106
+ y >= this.y && y <= this.y + this.wheelHeight);
107
+
108
+ if (!inside) {
109
+ return false;
110
+ }
111
+
112
+ // Activer le drag
113
+ this.dragging = true;
114
+ this.dragStartY = y;
115
+ this.lastY = y;
116
+
117
+ // Déterminer la roue
118
+ const wheelWidth = this.width / 3;
119
+ if (x < this.x + wheelWidth) {
120
+ this.dragWheel = 0; // Mois
121
+ } else if (x < this.x + wheelWidth * 2) {
122
+ this.dragWheel = 1; // Jour
123
+ } else {
124
+ this.dragWheel = 2; // Année
125
+ }
126
+
127
+ // Prendre le contrôle
128
+ this.framework.activeComponent = this;
129
+
130
+ // Ajouter l'écouteur global pour les mouvements
131
+ this._addGlobalMoveListener();
132
+
133
+ // Forcer le redessin
134
+ this._requestRedraw();
135
+
136
+ return true;
137
+ };
138
+
139
+ // Handler release
140
+ this.onRelease = (x, y) => {
141
+ if (this.dragging) {
142
+ this.dragging = false;
143
+ this.dragWheel = null;
144
+
145
+ // Retirer l'écouteur global
146
+ this._removeGlobalMoveListener();
147
+
148
+ // Relâcher le contrôle
149
+ if (this.framework.activeComponent === this) {
150
+ this.framework.activeComponent = null;
151
+ }
152
+
153
+ this._requestRedraw();
154
+ }
155
+ };
156
+
157
+ // Handler move du framework
158
+ this.onMove = (x, y) => {
159
+ // Laissé vide, on utilise l'écouteur global
160
+ };
161
+ }
162
+
163
+ /**
164
+ * Ajoute un écouteur global pour les mouvements
165
+ * @private
166
+ */
167
+ _addGlobalMoveListener() {
168
+ const canvas = this.framework.canvas;
169
+
170
+ // Sauvegarder les anciens handlers
171
+ this._savedMouseMove = canvas.onmousemove;
172
+ this._savedTouchMove = canvas.ontouchmove;
173
+
174
+ // Overrider les handlers
175
+ canvas.onmousemove = (e) => {
176
+ if (this.dragging) {
177
+ e.preventDefault();
178
+ e.stopPropagation();
179
+
180
+ const rect = canvas.getBoundingClientRect();
181
+ const x = e.clientX - rect.left;
182
+ const y = e.clientY - rect.top;
183
+
184
+ this._handleGlobalMove(x, y);
185
+ return false;
186
+ }
187
+
188
+ // Appeler le handler original si on ne drag pas
189
+ if (this._savedMouseMove) {
190
+ return this._savedMouseMove(e);
191
+ }
192
+ };
193
+
194
+ canvas.ontouchmove = (e) => {
195
+ if (this.dragging && e.touches.length > 0) {
196
+ e.preventDefault();
197
+ e.stopPropagation();
198
+
199
+ const touch = e.touches[0];
200
+ const rect = canvas.getBoundingClientRect();
201
+ const x = touch.clientX - rect.left;
202
+ const y = touch.clientY - rect.top;
203
+
204
+ this._handleGlobalMove(x, y);
205
+ return false;
206
+ }
207
+
208
+ if (this._savedTouchMove) {
209
+ return this._savedTouchMove(e);
210
+ }
211
+ };
212
+ }
213
+
214
+ /**
215
+ * Retire l'écouteur global
216
+ * @private
217
+ */
218
+ _removeGlobalMoveListener() {
219
+ const canvas = this.framework.canvas;
220
+
221
+ if (this._savedMouseMove) {
222
+ canvas.onmousemove = this._savedMouseMove;
223
+ this._savedMouseMove = null;
224
+ }
225
+
226
+ if (this._savedTouchMove) {
227
+ canvas.ontouchmove = this._savedTouchMove;
228
+ this._savedTouchMove = null;
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Gestionnaire de mouvement global
234
+ * @private
235
+ */
236
+ _handleGlobalMove(x, y) {
237
+ if (!this.dragging) return;
238
+
239
+ // Calculer le delta
240
+ const deltaY = y - this.lastY;
241
+ this.lastY = y;
242
+
243
+ // Appliquer le scroll si mouvement significatif
244
+ if (Math.abs(deltaY) > 0.5) {
245
+ const direction = deltaY > 0 ? 1 : -1;
246
+
247
+ // Appliquer le déplacement selon la roue avec limites
248
+ if (this.dragWheel === 0) {
249
+ // Mois - avec bouclage
250
+ let newMonth = this.monthWheel - direction;
251
+ if (newMonth < this.monthMin) newMonth = this.monthMax;
252
+ if (newMonth > this.monthMax) newMonth = this.monthMin;
253
+ this.monthWheel = newMonth;
254
+
255
+ // Mettre à jour les limites du jour après changement de mois
256
+ this._updateDayLimits();
257
+ }
258
+ else if (this.dragWheel === 1) {
259
+ // Jour - avec bouclage
260
+ let newDay = this.dayWheel - direction;
261
+ if (newDay < this.dayMin) newDay = this.dayMax;
262
+ if (newDay > this.dayMax) newDay = this.dayMin;
263
+ this.dayWheel = newDay;
264
+ }
265
+ else if (this.dragWheel === 2) {
266
+ // Année - avec limites strictes
267
+ let newYear = this.yearWheel - direction;
268
+ if (newYear < this.yearMin) newYear = this.yearMin;
269
+ if (newYear > this.yearMax) newYear = this.yearMax;
270
+ this.yearWheel = newYear;
271
+
272
+ // Mettre à jour les limites du jour après changement d'année
273
+ this._updateDayLimits();
274
+ }
275
+
276
+ // Mettre à jour la date
277
+ this._updateSelectedDate();
278
+
279
+ // Forcer le redessin
280
+ this._requestRedraw();
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Met à jour la date sélectionnée
286
+ * @private
287
+ */
288
+ _updateSelectedDate() {
289
+ // Créer la nouvelle date
290
+ const newDate = new Date(this.yearWheel, this.monthWheel, this.dayWheel);
291
+
292
+ // Vérifier si la date a changé
293
+ if (newDate.getTime() !== this.selectedDate.getTime()) {
294
+ this.selectedDate = newDate;
295
+
296
+ // Appeler le callback
297
+ if (this.onChange) {
298
+ this.onChange(this.selectedDate);
299
+ }
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Force le redessin du composant
305
+ * @private
306
+ */
307
+ _requestRedraw() {
308
+ if (this.framework.markComponentDirty) {
309
+ this.framework.markComponentDirty(this);
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Vérifie si un point est dans les limites du composant
315
+ * @param {number} x - Coordonnée X
316
+ * @param {number} y - Coordonnée Y
317
+ * @returns {boolean} True si le point est dans le composant
318
+ */
319
+ isPointInside(x, y) {
320
+ // Pas de scrollOffset car le Modal est fixe
321
+ return x >= this.x &&
322
+ x <= this.x + this.width &&
323
+ y >= this.y &&
324
+ y <= this.y + this.wheelHeight;
325
+ }
326
+
327
+ /**
328
+ * Dessine le sélecteur de date
329
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
330
+ */
331
+ draw(ctx) {
332
+ ctx.save();
333
+
334
+ const wheelWidth = this.width / 3;
335
+
336
+ // Fond
337
+ ctx.fillStyle = '#F9F9F9';
338
+ ctx.fillRect(this.x, this.y, this.width, this.wheelHeight);
339
+
340
+ // Bande de sélection
341
+ const selectionY = this.y + this.wheelHeight / 2 - this.itemHeight / 2;
342
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.05)';
343
+ ctx.fillRect(this.x, selectionY, this.width, this.itemHeight);
344
+
345
+ // Lignes de séparation
346
+ ctx.strokeStyle = '#C7C7CC';
347
+ ctx.lineWidth = 0.5;
348
+ ctx.beginPath();
349
+ ctx.moveTo(this.x, selectionY);
350
+ ctx.lineTo(this.x + this.width, selectionY);
351
+ ctx.moveTo(this.x, selectionY + this.itemHeight);
352
+ ctx.lineTo(this.x + this.width, selectionY + this.itemHeight);
353
+ ctx.stroke();
354
+
355
+ // Dividers verticaux
356
+ ctx.beginPath();
357
+ ctx.moveTo(this.x + wheelWidth, this.y);
358
+ ctx.lineTo(this.x + wheelWidth, this.y + this.wheelHeight);
359
+ ctx.moveTo(this.x + wheelWidth * 2, this.y);
360
+ ctx.lineTo(this.x + wheelWidth * 2, this.y + this.wheelHeight);
361
+ ctx.stroke();
362
+
363
+ // Mois (avec bouclage)
364
+ const monthNames = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
365
+ 'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
366
+ this._drawWheel(ctx, this.x, monthNames, this.monthWheel, this.monthMin, this.monthMax);
367
+
368
+ // Jour (avec ajustement dynamique)
369
+ const daysInMonth = new Date(this.yearWheel, this.monthWheel + 1, 0).getDate();
370
+ const days = Array.from({length: daysInMonth}, (_, i) => (i + 1).toString());
371
+ this._drawWheel(ctx, this.x + wheelWidth, days, this.dayWheel - 1, 0, daysInMonth - 1);
372
+
373
+ // Année (avec limites fixes)
374
+ const years = Array.from({length: this.yearMax - this.yearMin + 1},
375
+ (_, i) => (this.yearMin + i).toString());
376
+ const yearIndex = this.yearWheel - this.yearMin;
377
+ this._drawWheel(ctx, this.x + wheelWidth * 2, years, yearIndex, 0, years.length - 1);
378
+
379
+ ctx.restore();
380
+ }
381
+
382
+ /**
383
+ * Dessine une roue de sélection avec limites
384
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
385
+ * @param {number} x - Position X
386
+ * @param {string[]} items - Items à afficher
387
+ * @param {number} selectedIndex - Index sélectionné
388
+ * @param {number} minIndex - Index minimum
389
+ * @param {number} maxIndex - Index maximum
390
+ * @private
391
+ */
392
+ _drawWheel(ctx, x, items, selectedIndex, minIndex = 0, maxIndex = items.length - 1) {
393
+ const wheelWidth = this.width / 3;
394
+ const centerY = this.y + this.wheelHeight / 2;
395
+
396
+ ctx.save();
397
+ ctx.beginPath();
398
+ ctx.rect(x, this.y, wheelWidth, this.wheelHeight);
399
+ ctx.clip();
400
+
401
+ for (let i = -2; i <= 2; i++) {
402
+ const index = selectedIndex + i;
403
+ if (index >= minIndex && index <= maxIndex) {
404
+ const itemY = centerY + i * this.itemHeight;
405
+ const distance = Math.abs(itemY - centerY);
406
+ const scale = 1 - (distance / this.wheelHeight);
407
+ const opacity = Math.max(0.3, scale);
408
+
409
+ ctx.fillStyle = `rgba(0, 0, 0, ${opacity})`;
410
+ ctx.font = `${i === 0 ? 'bold ' : ''}${18 + scale * 2}px -apple-system, sans-serif`;
411
+ ctx.textAlign = 'center';
412
+ ctx.textBaseline = 'middle';
413
+ ctx.fillText(items[index - minIndex], x + wheelWidth / 2, itemY);
414
+ }
415
+ }
416
+
417
+ ctx.restore();
418
+ }
419
+
420
+ /**
421
+ * Nettoie le composant
422
+ * @private
423
+ */
424
+ _unmount() {
425
+ this._removeGlobalMoveListener();
426
+ super._unmount();
427
+ }
428
+ }
429
+
430
+ export default IOSDatePickerWheel;
@@ -0,0 +1,219 @@
1
+ import Component from '../core/Component.js';
2
+
3
+ /**
4
+ * Carousel / Slider d'images avec swipe horizontal et lazy load
5
+ * Compatible Material et Cupertino
6
+ * Tout le scroll est géré par le composant
7
+ */
8
+ class ImageCarousel extends Component {
9
+ constructor(framework, options = {}) {
10
+ super(framework, options);
11
+
12
+ this.images = options.images || [];
13
+ this.currentIndex = 0;
14
+ this.scrollX = 0;
15
+ this.height = options.height || 200;
16
+ this.spacing = options.spacing || 16;
17
+ this.borderRadius = options.borderRadius || 8;
18
+
19
+ this.pageIndicatorSize = options.pageIndicatorSize || 8;
20
+ this.pageIndicatorColor = options.pageIndicatorColor || '#6200EE';
21
+
22
+ this.platform = framework.platform;
23
+
24
+ this.isDragging = false;
25
+ this.lastX = 0;
26
+ this.velocity = 0;
27
+
28
+ this.onSwipeEnd = options.onSwipeEnd || null;
29
+ this.onImageClick = options.onImageClick || null;
30
+
31
+ this.loadedImages = Array(this.images.length).fill(null);
32
+
33
+ this._setupEventHandlers();
34
+ this.animateScroll();
35
+ }
36
+
37
+ // --------------------------
38
+ // Event handlers
39
+ // --------------------------
40
+ _setupEventHandlers() {
41
+ const canvas = this.framework.canvas;
42
+
43
+ // TOUCH
44
+ canvas.addEventListener('touchstart', (e) => {
45
+ if (e.touches.length === 1 && this.isPointInsideTouch(e.touches[0])) {
46
+ this.isDragging = true;
47
+ this.lastX = e.touches[0].clientX;
48
+ this.velocity = 0;
49
+ e.preventDefault();
50
+ }
51
+ });
52
+
53
+ canvas.addEventListener('touchmove', (e) => {
54
+ if (this.isDragging && e.touches.length === 1) {
55
+ const delta = e.touches[0].clientX - this.lastX;
56
+ this.scrollX += delta;
57
+ this.velocity = delta;
58
+ this.lastX = e.touches[0].clientX;
59
+
60
+ this._clampScroll();
61
+ this._requestRedraw();
62
+ e.preventDefault();
63
+ }
64
+ });
65
+
66
+ canvas.addEventListener('touchend', () => this._endDrag());
67
+
68
+ // MOUSE
69
+ canvas.addEventListener('mousedown', (e) => {
70
+ if (this.isPointInside(e)) {
71
+ this.isDragging = true;
72
+ this.lastX = e.clientX;
73
+ this.velocity = 0;
74
+ e.preventDefault();
75
+ }
76
+ });
77
+
78
+ canvas.addEventListener('mousemove', (e) => {
79
+ if (this.isDragging) {
80
+ const delta = e.clientX - this.lastX;
81
+ this.scrollX += delta;
82
+ this.velocity = delta;
83
+ this.lastX = e.clientX;
84
+
85
+ this._clampScroll();
86
+ this._requestRedraw();
87
+ }
88
+ });
89
+
90
+ canvas.addEventListener('mouseup', () => this._endDrag());
91
+ canvas.addEventListener('mouseleave', () => this._endDrag());
92
+ }
93
+
94
+ _endDrag() {
95
+ if (this.isDragging) {
96
+ this.isDragging = false;
97
+ // Snap à la page la plus proche
98
+ const targetIndex = Math.round(-this.scrollX / (this.width + this.spacing));
99
+ this.currentIndex = Math.min(Math.max(targetIndex, 0), this.images.length - 1);
100
+ this.scrollX = -this.currentIndex * (this.width + this.spacing);
101
+
102
+ if (this.onSwipeEnd) this.onSwipeEnd(this.currentIndex);
103
+ }
104
+ }
105
+
106
+ _clampScroll() {
107
+ const maxScroll = 0;
108
+ const minScroll = -(this.images.length - 1) * (this.width + this.spacing);
109
+ if (this.scrollX > maxScroll) this.scrollX = maxScroll;
110
+ if (this.scrollX < minScroll) this.scrollX = minScroll;
111
+ }
112
+
113
+ isPointInsideTouch(touch) {
114
+ const rect = this.framework.canvas.getBoundingClientRect();
115
+ const x = touch.clientX - rect.left;
116
+ const y = touch.clientY - rect.top;
117
+ return this.isPointInside(x, y);
118
+ }
119
+
120
+ isPointInside(x, y) {
121
+ return x >= this.x && x <= this.x + this.width &&
122
+ y >= this.y && y <= this.y + this.height;
123
+ }
124
+
125
+ _requestRedraw() {
126
+ if (this.framework.markComponentDirty) this.framework.markComponentDirty(this);
127
+ }
128
+
129
+ // --------------------------
130
+ // Animation / Inertie
131
+ // --------------------------
132
+ animateScroll() {
133
+ const animate = () => {
134
+ if (!this.isDragging) {
135
+ // inertia
136
+ if (Math.abs(this.velocity) > 0.1) {
137
+ this.scrollX += this.velocity;
138
+ this.velocity *= 0.95;
139
+
140
+ this._clampScroll();
141
+ } else {
142
+ // snap doux vers la page
143
+ const target = -this.currentIndex * (this.width + this.spacing);
144
+ this.scrollX += (target - this.scrollX) * 0.2;
145
+ }
146
+ }
147
+ requestAnimationFrame(animate);
148
+ };
149
+ animate();
150
+ }
151
+
152
+ // --------------------------
153
+ // Draw
154
+ // --------------------------
155
+ draw(ctx) {
156
+ ctx.save();
157
+ const startX = this.x + this.scrollX + this.spacing / 2;
158
+
159
+ for (let i = 0; i < this.images.length; i++) {
160
+ const imgX = startX + i * (this.width + this.spacing);
161
+
162
+ // lazy load
163
+ if (!this.loadedImages[i]) {
164
+ const img = new Image();
165
+ img.src = this.images[i];
166
+ img.onload = () => { this.loadedImages[i] = img; this._requestRedraw(); };
167
+ }
168
+
169
+ ctx.save();
170
+ ctx.beginPath();
171
+ this.roundRect(ctx, imgX, this.y, this.width, this.height, this.borderRadius);
172
+ ctx.clip();
173
+
174
+ if (this.loadedImages[i]) {
175
+ ctx.drawImage(this.loadedImages[i], imgX, this.y, this.width, this.height);
176
+ } else {
177
+ ctx.fillStyle = '#E0E0E0';
178
+ ctx.fillRect(imgX, this.y, this.width, this.height);
179
+ ctx.fillStyle = '#BDBDBD';
180
+ ctx.font = '20px sans-serif';
181
+ ctx.textAlign = 'center';
182
+ ctx.textBaseline = 'middle';
183
+ ctx.fillText('🖼', imgX + this.width / 2, this.y + this.height / 2);
184
+ }
185
+
186
+ ctx.restore();
187
+ }
188
+
189
+ // pagination Material
190
+ if (this.platform === 'material') {
191
+ const dotY = this.y + this.height + 12;
192
+ const totalWidth = this.images.length * this.pageIndicatorSize * 2;
193
+ const startDotX = this.x + (this.width - totalWidth) / 2;
194
+
195
+ for (let i = 0; i < this.images.length; i++) {
196
+ ctx.beginPath();
197
+ ctx.arc(startDotX + i * this.pageIndicatorSize * 2, dotY, this.pageIndicatorSize / 2, 0, Math.PI * 2);
198
+ ctx.fillStyle = i === this.currentIndex ? this.pageIndicatorColor : '#E0E0E0';
199
+ ctx.fill();
200
+ }
201
+ }
202
+
203
+ ctx.restore();
204
+ }
205
+
206
+ roundRect(ctx, x, y, width, height, radius) {
207
+ ctx.moveTo(x + radius, y);
208
+ ctx.lineTo(x + width - radius, y);
209
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
210
+ ctx.lineTo(x + width, y + height - radius);
211
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
212
+ ctx.lineTo(x + radius, y + height);
213
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
214
+ ctx.lineTo(x, y + radius);
215
+ ctx.quadraticCurveTo(x, y, x + radius, y);
216
+ }
217
+ }
218
+
219
+ export default ImageCarousel;