canvasframework 0.3.6

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 (85) hide show
  1. package/README.md +554 -0
  2. package/components/Accordion.js +252 -0
  3. package/components/AndroidDatePickerDialog.js +398 -0
  4. package/components/AppBar.js +225 -0
  5. package/components/Avatar.js +202 -0
  6. package/components/BottomNavigationBar.js +205 -0
  7. package/components/BottomSheet.js +374 -0
  8. package/components/Button.js +225 -0
  9. package/components/Card.js +193 -0
  10. package/components/Checkbox.js +180 -0
  11. package/components/Chip.js +212 -0
  12. package/components/CircularProgress.js +143 -0
  13. package/components/ContextMenu.js +116 -0
  14. package/components/DatePicker.js +257 -0
  15. package/components/Dialog.js +367 -0
  16. package/components/Divider.js +125 -0
  17. package/components/Drawer.js +261 -0
  18. package/components/FAB.js +270 -0
  19. package/components/FileUpload.js +315 -0
  20. package/components/IOSDatePickerWheel.js +268 -0
  21. package/components/ImageCarousel.js +193 -0
  22. package/components/ImageComponent.js +223 -0
  23. package/components/Input.js +309 -0
  24. package/components/List.js +94 -0
  25. package/components/ListItem.js +223 -0
  26. package/components/Modal.js +364 -0
  27. package/components/MultiSelectDialog.js +206 -0
  28. package/components/NumberInput.js +271 -0
  29. package/components/ProgressBar.js +88 -0
  30. package/components/RadioButton.js +142 -0
  31. package/components/SearchInput.js +315 -0
  32. package/components/SegmentedControl.js +202 -0
  33. package/components/Select.js +199 -0
  34. package/components/SelectDialog.js +255 -0
  35. package/components/Slider.js +113 -0
  36. package/components/Snackbar.js +243 -0
  37. package/components/Stepper.js +281 -0
  38. package/components/SwipeableListItem.js +179 -0
  39. package/components/Switch.js +147 -0
  40. package/components/Table.js +492 -0
  41. package/components/Tabs.js +125 -0
  42. package/components/Text.js +141 -0
  43. package/components/TextField.js +331 -0
  44. package/components/Toast.js +236 -0
  45. package/components/TreeView.js +420 -0
  46. package/components/Video.js +397 -0
  47. package/components/View.js +140 -0
  48. package/components/VirtualList.js +120 -0
  49. package/core/CanvasFramework.js +1271 -0
  50. package/core/CanvasWork.js +32 -0
  51. package/core/Component.js +153 -0
  52. package/core/LogicWorker.js +25 -0
  53. package/core/WebGLCanvasAdapter.js +1369 -0
  54. package/features/Column.js +43 -0
  55. package/features/Grid.js +47 -0
  56. package/features/LayoutComponent.js +43 -0
  57. package/features/OpenStreetMap.js +310 -0
  58. package/features/Positioned.js +33 -0
  59. package/features/PullToRefresh.js +328 -0
  60. package/features/Row.js +40 -0
  61. package/features/SignaturePad.js +257 -0
  62. package/features/Skeleton.js +84 -0
  63. package/features/Stack.js +21 -0
  64. package/index.js +101 -0
  65. package/manager/AccessibilityManager.js +107 -0
  66. package/manager/ErrorHandler.js +59 -0
  67. package/manager/FeatureFlags.js +60 -0
  68. package/manager/MemoryManager.js +107 -0
  69. package/manager/PerformanceMonitor.js +84 -0
  70. package/manager/SecurityManager.js +54 -0
  71. package/package.json +28 -0
  72. package/utils/AnimationEngine.js +428 -0
  73. package/utils/DataStore.js +403 -0
  74. package/utils/EventBus.js +407 -0
  75. package/utils/FetchClient.js +74 -0
  76. package/utils/FormValidator.js +355 -0
  77. package/utils/GeoLocationService.js +62 -0
  78. package/utils/I18n.js +207 -0
  79. package/utils/IndexedDBManager.js +273 -0
  80. package/utils/OfflineSyncManager.js +342 -0
  81. package/utils/QueryBuilder.js +478 -0
  82. package/utils/SafeArea.js +64 -0
  83. package/utils/SecureStorage.js +289 -0
  84. package/utils/StateManager.js +207 -0
  85. package/utils/WebSocketClient.js +66 -0
@@ -0,0 +1,374 @@
1
+ import Component from '../core/Component.js';
2
+ /**
3
+ * Composant BottomSheet (feuille modale depuis le bas) avec drag & drop
4
+ * @class
5
+ * @extends Component
6
+ * @param {Framework} framework - Instance du framework
7
+ * @param {Object} [options={}] - Options de configuration
8
+ * @param {number} [options.height=framework.height * 0.6] - Hauteur du bottom sheet
9
+ * @param {boolean} [options.dragHandle=true] - Afficher la poignée de drag
10
+ * @param {boolean} [options.closeOnOverlayClick=true] - Fermer au clic sur l'overlay
11
+ * @param {string} [options.bgColor='#FFFFFF'] - Couleur de fond
12
+ * @param {number} [options.borderRadius=16] - Rayon des coins arrondis
13
+ * @example
14
+ * const bottomSheet = new BottomSheet(framework, {
15
+ * height: 400,
16
+ * bgColor: '#F5F5F5',
17
+ * borderRadius: 20
18
+ * });
19
+ */
20
+ class BottomSheet extends Component {
21
+ /**
22
+ * @constructs BottomSheet
23
+ */
24
+ constructor(framework, options = {}) {
25
+ super(framework, {
26
+ x: 0,
27
+ y: framework.height,
28
+ width: framework.width,
29
+ height: options.height || framework.height * 0.6,
30
+ visible: false
31
+ });
32
+
33
+ /** @type {Component[]} */
34
+ this.children = [];
35
+ /** @type {boolean} */
36
+ this.dragHandle = options.dragHandle !== false;
37
+ /** @type {boolean} */
38
+ this.closeOnOverlayClick = options.closeOnOverlayClick !== false;
39
+ /** @type {string} */
40
+ this.bgColor = options.bgColor || '#FFFFFF';
41
+ /** @type {number} */
42
+ this.borderRadius = options.borderRadius || 16;
43
+ /** @type {number} */
44
+ this.targetY = framework.height;
45
+ /** @type {boolean} */
46
+ this.isOpen = false;
47
+ /** @type {boolean} */
48
+ this.animating = false;
49
+ /** @type {boolean} */
50
+ this.dragging = false;
51
+ /** @type {number} */
52
+ this.dragStartY = 0;
53
+ /** @type {number} */
54
+ this.dragOffset = 0;
55
+ /** @type {number} */
56
+ this.overlayOpacity = 0;
57
+
58
+ // IMPORTANT: Supprimer les bindings ici et les gérer différemment
59
+ this.onPress = this.handlePress.bind(this);
60
+ this.onMove = this.handleMove.bind(this);
61
+ this.onRelease = this.handleRelease.bind(this);
62
+
63
+ // Pour suivre le dernier clic
64
+ /** @type {number} */
65
+ this.lastClickTime = 0;
66
+ }
67
+
68
+ /**
69
+ * Ajoute un enfant au bottom sheet
70
+ * @param {Component} child - Composant enfant à ajouter
71
+ * @returns {Component} L'enfant ajouté
72
+ */
73
+ add(child) {
74
+ this.children.push(child);
75
+ return child;
76
+ }
77
+
78
+ /**
79
+ * Ouvre le bottom sheet avec animation
80
+ */
81
+ open() {
82
+ this.visible = true;
83
+ this.isOpen = true;
84
+ this.targetY = this.framework.height - this.height;
85
+ this.animate();
86
+ }
87
+
88
+ /**
89
+ * Ferme le bottom sheet avec animation
90
+ */
91
+ close() {
92
+ this.isOpen = false;
93
+ this.targetY = this.framework.height;
94
+ this.animate(() => {
95
+ this.visible = false;
96
+ });
97
+ }
98
+
99
+ /**
100
+ * Anime le bottom sheet vers sa position cible
101
+ * @param {Function} [callback] - Callback appelé à la fin de l'animation
102
+ * @private
103
+ */
104
+ animate(callback) {
105
+ if (this.animating) return;
106
+ this.animating = true;
107
+
108
+ const step = () => {
109
+ const diff = this.targetY - this.y;
110
+
111
+ if (Math.abs(diff) < 1) {
112
+ this.y = this.targetY;
113
+ this.overlayOpacity = this.isOpen ? 0.5 : 0;
114
+ this.animating = false;
115
+ if (callback) callback();
116
+ return;
117
+ }
118
+
119
+ this.y += diff * 0.2;
120
+
121
+ // Animer l'opacité de l'overlay
122
+ const progress = 1 - ((this.y - (this.framework.height - this.height)) / this.height);
123
+ this.overlayOpacity = Math.max(0, Math.min(0.5, progress * 0.5));
124
+
125
+ requestAnimationFrame(step);
126
+ };
127
+
128
+ step();
129
+ }
130
+
131
+ /**
132
+ * Gère le clic/touch sur le bottom sheet
133
+ * @param {number} x - Position X du clic
134
+ * @param {number} y - Position Y du clic
135
+ */
136
+ handlePress(x, y) {
137
+ // Empêcher les doubles clics rapides
138
+ const now = Date.now();
139
+ if (now - this.lastClickTime < 300) return;
140
+ this.lastClickTime = now;
141
+
142
+ // Calculer les coordonnées dans le sheet (sans scrollOffset car le sheet est fixe)
143
+ const adjustedY = y; // Pas d'ajustement de scroll pour le BottomSheet
144
+
145
+ // Clic sur l'overlay (zone sombre)
146
+ if (adjustedY < this.y && this.closeOnOverlayClick) {
147
+ this.close();
148
+ return;
149
+ }
150
+
151
+ // Début du drag sur la poignée
152
+ if (this.dragHandle && adjustedY >= this.y && adjustedY <= this.y + 40) {
153
+ this.dragging = true;
154
+ this.dragStartY = adjustedY;
155
+ this.dragOffset = 0;
156
+ this.framework.activeComponent = this;
157
+ return;
158
+ }
159
+
160
+ // Vérifier les clics sur les enfants
161
+ const contentY = this.y + (this.dragHandle ? 40 : 16);
162
+
163
+ // Parcourir les enfants dans l'ordre inverse (du dernier au premier)
164
+ for (let i = this.children.length - 1; i >= 0; i--) {
165
+ const child = this.children[i];
166
+
167
+ if (!child.visible) continue;
168
+
169
+ // Calculer les coordonnées absolues de l'enfant
170
+ const childAbsX = this.x + 16 + child.x;
171
+ const childAbsY = contentY + child.y;
172
+
173
+ // Vérifier si le clic est dans l'enfant
174
+ if (adjustedY >= childAbsY &&
175
+ adjustedY <= childAbsY + child.height &&
176
+ x >= childAbsX &&
177
+ x <= childAbsX + child.width) {
178
+
179
+ // Si l'enfant a un onClick, le déclencher
180
+ if (child.onClick) {
181
+ child.onClick();
182
+ return;
183
+ }
184
+
185
+ // Si l'enfant a un onPress, le déclencher
186
+ if (child.onPress) {
187
+ // Calculer les coordonnées relatives pour l'enfant
188
+ const relativeX = x - childAbsX;
189
+ const relativeY = adjustedY - childAbsY;
190
+ child.onPress(relativeX, relativeY);
191
+ return;
192
+ }
193
+
194
+ // Marquer l'enfant comme pressé pour l'effet visuel
195
+ child.pressed = true;
196
+
197
+ // Si c'est un bouton, déclencher son onClick après un délai
198
+ if (child instanceof Button || child instanceof FAB) {
199
+ setTimeout(() => {
200
+ if (child.onClick) child.onClick();
201
+ child.pressed = false;
202
+ }, 150);
203
+ }
204
+ return;
205
+ }
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Gère le déplacement pendant le drag
211
+ * @param {number} x - Position X actuelle
212
+ * @param {number} y - Position Y actuelle
213
+ */
214
+ handleMove(x, y) {
215
+ if (this.dragging) {
216
+ const adjustedY = y; // Pas d'ajustement de scroll
217
+ this.dragOffset = adjustedY - this.dragStartY;
218
+
219
+ // Limiter le drag vers le haut
220
+ const newY = (this.framework.height - this.height) + this.dragOffset;
221
+ if (newY >= this.framework.height - this.height) {
222
+ this.y = newY;
223
+
224
+ // Mettre à jour l'opacité de l'overlay
225
+ const progress = 1 - ((this.y - (this.framework.height - this.height)) / this.height);
226
+ this.overlayOpacity = Math.max(0, Math.min(0.5, progress * 0.5));
227
+ }
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Gère le relâchement après un drag
233
+ * @param {number} x - Position X du relâchement
234
+ * @param {number} y - Position Y du relâchement
235
+ */
236
+ handleRelease(x, y) {
237
+ if (this.dragging) {
238
+ this.dragging = false;
239
+ this.framework.activeComponent = null;
240
+
241
+ // Si on a dragué plus de 30% vers le bas, fermer
242
+ if (this.dragOffset > this.height * 0.3) {
243
+ this.close();
244
+ } else {
245
+ // Sinon, revenir à la position ouverte
246
+ this.targetY = this.framework.height - this.height;
247
+ this.animate();
248
+ }
249
+ }
250
+
251
+ // Réinitialiser l'état pressed pour tous les enfants
252
+ for (let child of this.children) {
253
+ child.pressed = false;
254
+ }
255
+ }
256
+
257
+ /**
258
+ * Dessine le bottom sheet
259
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
260
+ */
261
+ draw(ctx) {
262
+ if (!this.visible) return;
263
+
264
+ ctx.save();
265
+
266
+ // Overlay sombre
267
+ ctx.fillStyle = `rgba(0, 0, 0, ${this.overlayOpacity})`;
268
+ ctx.fillRect(0, 0, this.framework.width, this.framework.height);
269
+
270
+ // BottomSheet
271
+ ctx.fillStyle = this.bgColor;
272
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
273
+ ctx.shadowBlur = 20;
274
+ ctx.shadowOffsetY = -5;
275
+
276
+ ctx.beginPath();
277
+ this.roundRectTop(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
278
+ ctx.fill();
279
+
280
+ ctx.shadowColor = 'transparent';
281
+
282
+ // Drag Handle
283
+ if (this.dragHandle) {
284
+ ctx.fillStyle = '#CCCCCC';
285
+ ctx.beginPath();
286
+ this.roundRect(ctx, this.width / 2 - 20, this.y + 12, 40, 4, 2);
287
+ ctx.fill();
288
+ }
289
+
290
+ // Contenu (avec clipping)
291
+ const contentY = this.y + (this.dragHandle ? 40 : 16);
292
+ const contentHeight = this.height - (this.dragHandle ? 40 : 16);
293
+
294
+ ctx.save();
295
+ ctx.beginPath();
296
+ ctx.rect(this.x, contentY, this.width, contentHeight);
297
+ ctx.clip();
298
+
299
+ // Dessiner les enfants
300
+ for (let child of this.children) {
301
+ if (child.visible) {
302
+ const originalX = child.x;
303
+ const originalY = child.y;
304
+
305
+ child.x = this.x + 16 + originalX;
306
+ child.y = contentY + originalY;
307
+
308
+ child.draw(ctx);
309
+
310
+ child.x = originalX;
311
+ child.y = originalY;
312
+ }
313
+ }
314
+
315
+ ctx.restore();
316
+ ctx.restore();
317
+ }
318
+
319
+ /**
320
+ * Dessine un rectangle avec seulement les coins supérieurs arrondis
321
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
322
+ * @param {number} x - Position X
323
+ * @param {number} y - Position Y
324
+ * @param {number} width - Largeur
325
+ * @param {number} height - Hauteur
326
+ * @param {number} radius - Rayon des coins
327
+ * @private
328
+ */
329
+ roundRectTop(ctx, x, y, width, height, radius) {
330
+ ctx.moveTo(x + radius, y);
331
+ ctx.lineTo(x + width - radius, y);
332
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
333
+ ctx.lineTo(x + width, y + height);
334
+ ctx.lineTo(x, y + height);
335
+ ctx.lineTo(x, y + radius);
336
+ ctx.quadraticCurveTo(x, y, x + radius, y);
337
+ }
338
+
339
+ /**
340
+ * Dessine un rectangle avec des coins arrondis
341
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
342
+ * @param {number} x - Position X
343
+ * @param {number} y - Position Y
344
+ * @param {number} width - Largeur
345
+ * @param {number} height - Hauteur
346
+ * @param {number} radius - Rayon des coins
347
+ * @private
348
+ */
349
+ roundRect(ctx, x, y, width, height, radius) {
350
+ ctx.beginPath();
351
+ ctx.moveTo(x + radius, y);
352
+ ctx.lineTo(x + width - radius, y);
353
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
354
+ ctx.lineTo(x + width, y + height - radius);
355
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
356
+ ctx.lineTo(x + radius, y + height);
357
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
358
+ ctx.lineTo(x, y + radius);
359
+ ctx.quadraticCurveTo(x, y, x + radius, y);
360
+ ctx.closePath();
361
+ }
362
+
363
+ /**
364
+ * Vérifie si un point est à l'intérieur du composant
365
+ * @param {number} x - Position X
366
+ * @param {number} y - Position Y
367
+ * @returns {boolean} Toujours true si visible (le composant occupe tout l'écran)
368
+ */
369
+ isPointInside(x, y) {
370
+ return this.visible;
371
+ }
372
+ }
373
+
374
+ export default BottomSheet;
@@ -0,0 +1,225 @@
1
+ import Component from '../core/Component.js';
2
+
3
+ /**
4
+ * Bouton cliquable
5
+ * @class
6
+ * @extends Component
7
+ * @property {string} text - Texte du bouton
8
+ * @property {number} fontSize - Taille de la police
9
+ * @property {string} platform - Plateforme
10
+ * @property {string} bgColor - Couleur de fond
11
+ * @property {string} textColor - Couleur du texte
12
+ * @property {string} rippleColor - Couleur du ripple
13
+ * @property {number} elevation - Élévation (ombre)
14
+ * @property {Array} ripples - Effets ripple
15
+ */
16
+ class Button extends Component {
17
+ /**
18
+ * Crée une instance de Button
19
+ * @param {CanvasFramework} framework - Framework parent
20
+ * @param {Object} [options={}] - Options de configuration
21
+ * @param {string} [options.text='Button'] - Texte
22
+ * @param {number} [options.fontSize=16] - Taille de police
23
+ * @param {string} [options.bgColor] - Couleur de fond (auto selon platform)
24
+ * @param {string} [options.textColor] - Couleur du texte (auto selon platform)
25
+ * @param {number} [options.elevation=2] - Élévation (Material)
26
+ */
27
+ constructor(framework, options = {}) {
28
+ super(framework, options);
29
+ this.text = options.text || 'Button';
30
+ this.fontSize = options.fontSize || 16;
31
+ this.platform = framework.platform;
32
+
33
+ // Couleurs selon la plateforme
34
+ if (this.platform === 'material') {
35
+ this.bgColor = options.bgColor || '#6200EE';
36
+ this.textColor = options.textColor || '#FFFFFF';
37
+ this.rippleColor = 'rgba(255, 255, 255, 0.3)';
38
+ this.elevation = options.elevation || 2;
39
+ } else {
40
+ this.bgColor = options.bgColor || '#007AFF';
41
+ this.textColor = options.textColor || '#FFFFFF';
42
+ this.borderRadius = 10;
43
+ }
44
+
45
+ // Effet Ripple
46
+ this.ripples = [];
47
+
48
+ // Bind des méthodes
49
+ this.onPress = this.handlePress.bind(this);
50
+ }
51
+
52
+ /**
53
+ * Gère la pression sur le bouton
54
+ * @param {number} x - Coordonnée X
55
+ * @param {number} y - Coordonnée Y
56
+ * @private
57
+ */
58
+ handlePress(x, y) {
59
+ // Créer un ripple au point de clic
60
+ if (this.platform === 'material') {
61
+ const adjustedY = y - this.framework.scrollOffset;
62
+ this.ripples.push({
63
+ x: x - this.x,
64
+ y: adjustedY - this.y,
65
+ radius: 0,
66
+ maxRadius: Math.max(this.width, this.height) * 1.5,
67
+ opacity: 1
68
+ });
69
+ this.animateRipple();
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Anime les effets ripple
75
+ * @private
76
+ */
77
+ animateRipple() {
78
+ const animate = () => {
79
+ let hasActiveRipples = false;
80
+
81
+ for (let ripple of this.ripples) {
82
+ if (ripple.radius < ripple.maxRadius) {
83
+ ripple.radius += ripple.maxRadius / 15;
84
+ hasActiveRipples = true;
85
+ }
86
+
87
+ if (ripple.radius >= ripple.maxRadius * 0.5) {
88
+ ripple.opacity -= 0.05;
89
+ }
90
+ }
91
+
92
+ // Nettoyer les ripples terminés
93
+ this.ripples = this.ripples.filter(r => r.opacity > 0);
94
+
95
+ if (hasActiveRipples) {
96
+ requestAnimationFrame(animate);
97
+ }
98
+ };
99
+
100
+ animate();
101
+ }
102
+
103
+ /**
104
+ * Dessine le bouton
105
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
106
+ */
107
+ draw(ctx) {
108
+ ctx.save();
109
+
110
+ if (this.platform === 'material') {
111
+ // Material Design
112
+ // Ombre (elevation)
113
+ if (this.elevation > 0 && !this.pressed) {
114
+ ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
115
+ ctx.shadowBlur = this.elevation * 2;
116
+ ctx.shadowOffsetY = this.elevation;
117
+ }
118
+
119
+ ctx.fillStyle = this.pressed ? this.darkenColor(this.bgColor) : this.bgColor;
120
+ ctx.beginPath();
121
+ this.roundRect(ctx, this.x, this.y, this.width, this.height, 4);
122
+ ctx.fill();
123
+
124
+ ctx.shadowColor = 'transparent';
125
+ ctx.shadowBlur = 0;
126
+ ctx.shadowOffsetY = 0;
127
+
128
+ // Clipping pour les ripples
129
+ ctx.save();
130
+ ctx.beginPath();
131
+ this.roundRect(ctx, this.x, this.y, this.width, this.height, 4);
132
+ ctx.clip();
133
+
134
+ // Dessiner les ripples
135
+ for (let ripple of this.ripples) {
136
+ ctx.globalAlpha = ripple.opacity;
137
+ ctx.fillStyle = this.rippleColor;
138
+ ctx.beginPath();
139
+ ctx.arc(this.x + ripple.x, this.y + ripple.y, ripple.radius, 0, Math.PI * 2);
140
+ ctx.fill();
141
+ }
142
+
143
+ ctx.restore();
144
+
145
+ } else {
146
+ // Cupertino (iOS)
147
+ ctx.fillStyle = this.pressed ? this.darkenColor(this.bgColor) : this.bgColor;
148
+ ctx.beginPath();
149
+ this.roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
150
+ ctx.fill();
151
+ }
152
+
153
+ // Texte
154
+ ctx.fillStyle = this.textColor;
155
+ ctx.font = `${this.fontSize}px -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif`;
156
+ ctx.textAlign = 'center';
157
+ ctx.textBaseline = 'middle';
158
+ ctx.fillText(this.text, this.x + this.width / 2, this.y + this.height / 2);
159
+
160
+ ctx.restore();
161
+ }
162
+
163
+ /**
164
+ * Dessine un rectangle avec coins arrondis
165
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
166
+ * @param {number} x - Position X
167
+ * @param {number} y - Position Y
168
+ * @param {number} width - Largeur
169
+ * @param {number} height - Hauteur
170
+ * @param {number} radius - Rayon des coins
171
+ * @private
172
+ */
173
+ roundRect(ctx, x, y, width, height, radius) {
174
+ ctx.moveTo(x + radius, y);
175
+ ctx.lineTo(x + width - radius, y);
176
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
177
+ ctx.lineTo(x + width, y + height - radius);
178
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
179
+ ctx.lineTo(x + radius, y + height);
180
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
181
+ ctx.lineTo(x, y + radius);
182
+ ctx.quadraticCurveTo(x, y, x + radius, y);
183
+ }
184
+
185
+ /**
186
+ * Assombrit une couleur
187
+ * @param {string} color - Couleur hexadécimale
188
+ * @returns {string} Couleur assombrie
189
+ * @private
190
+ */
191
+ darkenColor(color) {
192
+ const rgb = this.hexToRgb(color);
193
+ return `rgb(${Math.max(0, rgb.r - 30)}, ${Math.max(0, rgb.g - 30)}, ${Math.max(0, rgb.b - 30)})`;
194
+ }
195
+
196
+ /**
197
+ * Convertit une couleur hex en RGB
198
+ * @param {string} hex - Couleur hexadécimale
199
+ * @returns {{r: number, g: number, b: number}} Objet RGB
200
+ * @private
201
+ */
202
+ hexToRgb(hex) {
203
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
204
+ return result ? {
205
+ r: parseInt(result[1], 16),
206
+ g: parseInt(result[2], 16),
207
+ b: parseInt(result[3], 16)
208
+ } : { r: 0, g: 0, b: 0 };
209
+ }
210
+
211
+ /**
212
+ * Vérifie si un point est dans les limites
213
+ * @param {number} x - Coordonnée X
214
+ * @param {number} y - Coordonnée Y
215
+ * @returns {boolean} True si le point est dans le bouton
216
+ */
217
+ isPointInside(x, y) {
218
+ return x >= this.x &&
219
+ x <= this.x + this.width &&
220
+ y >= this.y &&
221
+ y <= this.y + this.height;
222
+ }
223
+ }
224
+
225
+ export default Button;