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,166 @@
1
+ import Component from '../core/Component.js';
2
+
3
+ /**
4
+ * Checkbox Material & Cupertino (iOS-like)
5
+ */
6
+ class Checkbox extends Component {
7
+ constructor(framework, options = {}) {
8
+ super(framework, options);
9
+
10
+ this.checked = !!options.checked;
11
+ this.label = options.label || '';
12
+ this.platform = framework.platform;
13
+ this.onChange = options.onChange;
14
+
15
+ this.boxSize = 22;
16
+ this.padding = 10;
17
+
18
+ this.textWidth = this.label
19
+ ? this.getTextWidth(this.label)
20
+ : 0;
21
+
22
+ // Largeur totale
23
+ this.width =
24
+ this.platform === 'material'
25
+ ? this.boxSize + this.padding + this.textWidth
26
+ : this.textWidth + 28; // place pour checkmark iOS
27
+
28
+ this.height = 28;
29
+
30
+ this.onClick = () => {
31
+ this.checked = !this.checked;
32
+ this.onChange?.(this.checked);
33
+ };
34
+ }
35
+
36
+ getTextWidth(text) {
37
+ const ctx = this.framework.ctx;
38
+ ctx.save();
39
+ ctx.font = '16px -apple-system, system-ui, sans-serif';
40
+ const w = ctx.measureText(text).width;
41
+ ctx.restore();
42
+ return w;
43
+ }
44
+
45
+ draw(ctx) {
46
+ ctx.save();
47
+ ctx.font = '16px -apple-system, system-ui, sans-serif';
48
+ ctx.textBaseline = 'middle';
49
+
50
+ const centerY = this.y + this.height / 2;
51
+
52
+ if (this.platform === 'material') {
53
+ this.drawMaterial(ctx, centerY);
54
+ } else {
55
+ this.drawCupertino(ctx, centerY);
56
+ }
57
+
58
+ ctx.restore();
59
+ }
60
+
61
+ /* ---------------- MATERIAL ---------------- */
62
+
63
+ drawMaterial(ctx, centerY) {
64
+ const x = this.x;
65
+ const y = centerY - this.boxSize / 2;
66
+
67
+ // Box
68
+ ctx.lineWidth = 2;
69
+ ctx.strokeStyle = this.checked ? '#6200EE' : '#757575';
70
+ ctx.fillStyle = this.checked ? '#6200EE' : 'transparent';
71
+
72
+ this.roundRect(ctx, x, y, this.boxSize, this.boxSize, 3);
73
+ if (this.checked) ctx.fill();
74
+ ctx.stroke();
75
+
76
+ // Check
77
+ if (this.checked) {
78
+ ctx.strokeStyle = '#FFF';
79
+ ctx.lineWidth = 2.4;
80
+ ctx.beginPath();
81
+ ctx.moveTo(x + 5, y + 12);
82
+ ctx.lineTo(x + 9, y + 16);
83
+ ctx.lineTo(x + 17, y + 7);
84
+ ctx.stroke();
85
+ }
86
+
87
+ // Label
88
+ ctx.fillStyle = '#000';
89
+ ctx.fillText(
90
+ this.label,
91
+ x + this.boxSize + this.padding,
92
+ centerY
93
+ );
94
+ }
95
+
96
+ /* ---------------- CUPERTINO ---------------- */
97
+
98
+ /* ---------------- CUPERTINO ---------------- */
99
+
100
+ drawCupertino(ctx, centerY) {
101
+ const radius = 10;
102
+ const circleX = this.x + radius;
103
+ const circleY = centerY;
104
+
105
+ // Cercle
106
+ if (this.checked) {
107
+ ctx.fillStyle = '#007AFF'; // Apple blue
108
+ ctx.beginPath();
109
+ ctx.arc(circleX, circleY, radius, 0, Math.PI * 2);
110
+ ctx.fill();
111
+ } else {
112
+ ctx.strokeStyle = '#C7C7CC'; // iOS gray
113
+ ctx.lineWidth = 2;
114
+ ctx.beginPath();
115
+ ctx.arc(circleX, circleY, radius, 0, Math.PI * 2);
116
+ ctx.stroke();
117
+ }
118
+
119
+ // Checkmark
120
+ if (this.checked) {
121
+ ctx.strokeStyle = '#FFFFFF';
122
+ ctx.lineWidth = 2.2;
123
+ ctx.lineCap = 'round';
124
+ ctx.lineJoin = 'round';
125
+
126
+ ctx.beginPath();
127
+ ctx.moveTo(circleX - 4, circleY);
128
+ ctx.lineTo(circleX - 1, circleY + 3);
129
+ ctx.lineTo(circleX + 5, circleY - 4);
130
+ ctx.stroke();
131
+ }
132
+
133
+ // Label
134
+ ctx.fillStyle = '#000';
135
+ ctx.fillText(
136
+ this.label,
137
+ this.x + radius * 2 + this.padding,
138
+ centerY
139
+ );
140
+ }
141
+
142
+ roundRect(ctx, x, y, w, h, r) {
143
+ ctx.beginPath();
144
+ ctx.moveTo(x + r, y);
145
+ ctx.lineTo(x + w - r, y);
146
+ ctx.quadraticCurveTo(x + w, y, x + w, y + r);
147
+ ctx.lineTo(x + w, y + h - r);
148
+ ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
149
+ ctx.lineTo(x + r, y + h);
150
+ ctx.quadraticCurveTo(x, y + h, x, y + h - r);
151
+ ctx.lineTo(x, y + r);
152
+ ctx.quadraticCurveTo(x, y, x + r, y);
153
+ ctx.closePath();
154
+ }
155
+
156
+ isPointInside(x, y) {
157
+ return (
158
+ x >= this.x &&
159
+ x <= this.x + this.width &&
160
+ y >= this.y &&
161
+ y <= this.y + this.height
162
+ );
163
+ }
164
+ }
165
+
166
+ export default Checkbox;
@@ -0,0 +1,212 @@
1
+ import Component from '../core/Component.js';
2
+ /**
3
+ * Chip (étiquette cliquable)
4
+ * @class
5
+ * @extends Component
6
+ * @property {string} text - Texte
7
+ * @property {string|null} icon - Icône
8
+ * @property {boolean} closable - Peut être fermé
9
+ * @property {string} platform - Plateforme
10
+ * @property {string} bgColor - Couleur de fond
11
+ * @property {string} textColor - Couleur du texte
12
+ * @property {Function} onClose - Callback à la fermeture
13
+ * @property {number} borderRadius - Rayon des coins
14
+ * @property {Object|null} closeButtonRect - Rectangle du bouton fermer
15
+ */
16
+ class Chip extends Component {
17
+ /**
18
+ * Crée une instance de Chip
19
+ * @param {CanvasFramework} framework - Framework parent
20
+ * @param {Object} [options={}] - Options de configuration
21
+ * @param {string} [options.text=''] - Texte
22
+ * @param {string} [options.icon] - Icône
23
+ * @param {boolean} [options.closable=true] - Peut être fermé
24
+ * @param {string} [options.bgColor] - Couleur de fond (auto selon platform)
25
+ * @param {string} [options.textColor='#000000'] - Couleur du texte
26
+ * @param {Function} [options.onClose] - Callback à la fermeture
27
+ * @param {number} [options.height=32] - Hauteur
28
+ */
29
+ constructor(framework, options = {}) {
30
+ super(framework, options);
31
+ this.text = options.text || '';
32
+ this.icon = options.icon || null;
33
+ this.closable = options.closable !== false;
34
+ this.platform = framework.platform;
35
+ this.bgColor = options.bgColor || (framework.platform === 'material' ? '#E0E0E0' : '#F0F0F0');
36
+ this.textColor = options.textColor || '#000000';
37
+ this.onClose = options.onClose;
38
+
39
+ // Calculer la largeur en fonction du contenu
40
+ const ctx = framework.ctx;
41
+ ctx.font = '14px -apple-system, sans-serif';
42
+ const textWidth = ctx.measureText(this.text).width;
43
+ const iconWidth = this.icon ? 24 : 0;
44
+ const closeWidth = this.closable ? 24 : 0;
45
+ this.width = iconWidth + textWidth + closeWidth + 24; // padding
46
+ this.height = options.height || 32;
47
+ this.borderRadius = this.height / 2;
48
+
49
+ this.closeButtonRect = null;
50
+ this.onPress = this.handlePress.bind(this);
51
+ }
52
+
53
+ /**
54
+ * Dessine le chip
55
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
56
+ */
57
+ draw(ctx) {
58
+ ctx.save();
59
+
60
+ // Background
61
+ ctx.fillStyle = this.pressed ? this.darkenColor(this.bgColor) : this.bgColor;
62
+ ctx.beginPath();
63
+ this.roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
64
+ ctx.fill();
65
+
66
+ let currentX = this.x + 12;
67
+
68
+ // Icône
69
+ if (this.icon) {
70
+ ctx.font = '16px sans-serif';
71
+ ctx.textAlign = 'left';
72
+ ctx.textBaseline = 'middle';
73
+ ctx.fillStyle = this.textColor;
74
+ ctx.fillText(this.icon, currentX, this.y + this.height / 2);
75
+ currentX += 20;
76
+ }
77
+
78
+ // Texte
79
+ ctx.font = '14px -apple-system, sans-serif';
80
+ ctx.fillStyle = this.textColor;
81
+ ctx.textAlign = 'left';
82
+ ctx.textBaseline = 'middle';
83
+ ctx.fillText(this.text, currentX, this.y + this.height / 2);
84
+
85
+ // Bouton de fermeture
86
+ if (this.closable) {
87
+ const closeX = this.x + this.width - 20;
88
+ const closeY = this.y + this.height / 2;
89
+
90
+ this.closeButtonRect = {
91
+ x: closeX - 8,
92
+ y: closeY - 8,
93
+ width: 16,
94
+ height: 16
95
+ };
96
+
97
+ // Cercle du bouton (optionnel)
98
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
99
+ ctx.beginPath();
100
+ ctx.arc(closeX, closeY, 8, 0, Math.PI * 2);
101
+ ctx.fill();
102
+
103
+ // Croix (X)
104
+ ctx.strokeStyle = this.textColor;
105
+ ctx.lineWidth = 1.5;
106
+ ctx.lineCap = 'round';
107
+ ctx.beginPath();
108
+ ctx.moveTo(closeX - 4, closeY - 4);
109
+ ctx.lineTo(closeX + 4, closeY + 4);
110
+ ctx.stroke();
111
+
112
+ ctx.beginPath();
113
+ ctx.moveTo(closeX + 4, closeY - 4);
114
+ ctx.lineTo(closeX - 4, closeY + 4);
115
+ ctx.stroke();
116
+ }
117
+
118
+ ctx.restore();
119
+ }
120
+
121
+ /**
122
+ * Gère la pression (clic)
123
+ * @param {number} x - Coordonnée X
124
+ * @param {number} y - Coordonnée Y
125
+ * @private
126
+ */
127
+ handlePress(x, y) {
128
+ const adjustedY = y - this.framework.scrollOffset;
129
+
130
+ // Vérifier si on clique sur le bouton de fermeture
131
+ if (this.closable && this.closeButtonRect) {
132
+ if (x >= this.closeButtonRect.x &&
133
+ x <= this.closeButtonRect.x + this.closeButtonRect.width &&
134
+ adjustedY >= this.closeButtonRect.y &&
135
+ adjustedY <= this.closeButtonRect.y + this.closeButtonRect.height) {
136
+ if (this.onClose) this.onClose();
137
+ return;
138
+ }
139
+ }
140
+
141
+ // Sinon, déclencher onClick normal
142
+ if (this.onClick) this.onClick();
143
+ }
144
+
145
+ /**
146
+ * Dessine un rectangle avec coins arrondis
147
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
148
+ * @param {number} x - Position X
149
+ * @param {number} y - Position Y
150
+ * @param {number} width - Largeur
151
+ * @param {number} height - Hauteur
152
+ * @param {number} radius - Rayon des coins
153
+ * @private
154
+ */
155
+ roundRect(ctx, x, y, width, height, radius) {
156
+ ctx.beginPath();
157
+ ctx.moveTo(x + radius, y);
158
+ ctx.lineTo(x + width - radius, y);
159
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
160
+ ctx.lineTo(x + width, y + height - radius);
161
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
162
+ ctx.lineTo(x + radius, y + height);
163
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
164
+ ctx.lineTo(x, y + radius);
165
+ ctx.quadraticCurveTo(x, y, x + radius, y);
166
+ ctx.closePath();
167
+ }
168
+
169
+ /**
170
+ * Assombrit une couleur
171
+ * @param {string} color - Couleur
172
+ * @returns {string} Couleur assombrie
173
+ * @private
174
+ */
175
+ darkenColor(color) {
176
+ // Utiliser la même méthode que Button
177
+ if (color.startsWith('#')) {
178
+ const rgb = this.hexToRgb(color);
179
+ return `rgb(${Math.max(0, rgb.r - 20)}, ${Math.max(0, rgb.g - 20)}, ${Math.max(0, rgb.b - 20)})`;
180
+ }
181
+ return color;
182
+ }
183
+
184
+ /**
185
+ * Convertit une couleur hex en RGB
186
+ * @param {string} hex - Couleur hexadécimale
187
+ * @returns {{r: number, g: number, b: number}} Objet RGB
188
+ * @private
189
+ */
190
+ hexToRgb(hex) {
191
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
192
+ return result ? {
193
+ r: parseInt(result[1], 16),
194
+ g: parseInt(result[2], 16),
195
+ b: parseInt(result[3], 16)
196
+ } : { r: 0, g: 0, b: 0 };
197
+ }
198
+
199
+ /**
200
+ * Vérifie si un point est dans les limites
201
+ * @param {number} x - Coordonnée X
202
+ * @param {number} y - Coordonnée Y
203
+ * @returns {boolean} True si le point est dans le chip
204
+ */
205
+ isPointInside(x, y) {
206
+ const adjustedY = y - this.framework.scrollOffset;
207
+ return x >= this.x && x <= this.x + this.width &&
208
+ adjustedY >= this.y && adjustedY <= this.y + this.height;
209
+ }
210
+ }
211
+
212
+ export default Chip;
@@ -0,0 +1,327 @@
1
+ import Component from '../core/Component.js';
2
+
3
+ /**
4
+ * Spinner de chargement circulaire avec support Material et Cupertino
5
+ * @class
6
+ * @extends Component
7
+ * @property {number} size - Taille du spinner
8
+ * @property {boolean} indeterminate - Mode indéterminé
9
+ * @property {number} progress - Progression (0-100)
10
+ * @property {string} platform - Plateforme
11
+ * @property {string} color - Couleur
12
+ * @property {number} lineWidth - Épaisseur de la ligne
13
+ * @property {number} rotation - Rotation actuelle
14
+ * @property {number} animationSpeed - Vitesse d'animation
15
+ */
16
+ class CircularProgress extends Component {
17
+ /**
18
+ * Crée une instance de CircularProgress
19
+ * @param {CanvasFramework} framework - Framework parent
20
+ * @param {Object} [options={}] - Options de configuration
21
+ * @param {number} [options.size=40] - Taille
22
+ * @param {boolean} [options.indeterminate=true] - Mode indéterminé
23
+ * @param {number} [options.progress=0] - Progression (0-100)
24
+ * @param {string} [options.color] - Couleur (auto selon platform)
25
+ * @param {number} [options.lineWidth] - Épaisseur (auto selon platform)
26
+ * @param {number} [options.animationSpeed] - Vitesse d'animation (auto selon platform)
27
+ */
28
+ constructor(framework, options = {}) {
29
+ super(framework, options);
30
+ this.size = options.size || 40;
31
+ this.indeterminate = options.indeterminate !== false;
32
+ this.progress = options.progress || 0; // 0-100
33
+ this.platform = framework.platform;
34
+
35
+ // Couleurs selon la plateforme
36
+ this.color = options.color || (
37
+ this.platform === 'material' ? '#6200EE' : '#8E8E93'
38
+ );
39
+
40
+ // Épaisseur selon la plateforme
41
+ this.lineWidth = options.lineWidth || (
42
+ this.platform === 'material' ? 4 : 2.5
43
+ );
44
+
45
+ // Vitesse d'animation selon la plateforme
46
+ this.animationSpeed = options.animationSpeed || (
47
+ this.platform === 'material' ? 0.05 : 0.08
48
+ );
49
+
50
+ this.rotation = 0;
51
+
52
+ // Pour l'animation Material (arc qui s'agrandit/rétrécit)
53
+ this.arcStart = 0;
54
+ this.arcEnd = 0;
55
+ this.arcGrowing = true;
56
+
57
+ // Pour l'animation Cupertino (12 traits qui tournent)
58
+ this.cupertinoLines = 12;
59
+ this.cupertinoOpacity = Array(this.cupertinoLines).fill(0);
60
+
61
+ this.width = this.size;
62
+ this.height = this.size;
63
+
64
+ // Démarrer l'animation pour indeterminate
65
+ if (this.indeterminate) {
66
+ this.startAnimation();
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Démarre l'animation du spinner
72
+ * @private
73
+ */
74
+ startAnimation() {
75
+ let lastTime = performance.now();
76
+
77
+ const animate = (currentTime) => {
78
+ if (!this.visible || !this.indeterminate) return;
79
+
80
+ const deltaTime = currentTime - lastTime;
81
+ lastTime = currentTime;
82
+
83
+ if (this.platform === 'material') {
84
+ this.animateMaterial(deltaTime);
85
+ } else {
86
+ this.animateCupertino(deltaTime);
87
+ }
88
+
89
+ this.markDirty();
90
+ requestAnimationFrame(animate);
91
+ };
92
+
93
+ requestAnimationFrame(animate);
94
+ }
95
+
96
+ /**
97
+ * Animation Material (arc qui tourne et change de taille)
98
+ * @private
99
+ */
100
+ animateMaterial(deltaTime) {
101
+ // Rotation globale
102
+ this.rotation += this.animationSpeed;
103
+ if (this.rotation > Math.PI * 2) {
104
+ this.rotation -= Math.PI * 2;
105
+ }
106
+
107
+ // Animation de l'arc (grossit puis rétrécit)
108
+ const arcSpeed = 0.03;
109
+
110
+ if (this.arcGrowing) {
111
+ this.arcEnd += arcSpeed;
112
+ if (this.arcEnd > Math.PI * 1.5) {
113
+ this.arcGrowing = false;
114
+ }
115
+ } else {
116
+ this.arcStart += arcSpeed;
117
+ if (this.arcStart >= this.arcEnd) {
118
+ this.arcGrowing = true;
119
+ this.arcStart = 0;
120
+ this.arcEnd = 0;
121
+ }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Animation Cupertino (traits qui s'estompent)
127
+ * @private
128
+ */
129
+ animateCupertino(deltaTime) {
130
+ this.rotation += this.animationSpeed;
131
+ if (this.rotation > Math.PI * 2) {
132
+ this.rotation -= Math.PI * 2;
133
+ }
134
+
135
+ // Calculer l'opacité de chaque trait (fade progressif)
136
+ const activeIndex = Math.floor((this.rotation / (Math.PI * 2)) * this.cupertinoLines);
137
+
138
+ for (let i = 0; i < this.cupertinoLines; i++) {
139
+ const distance = Math.abs(i - activeIndex);
140
+ const minDistance = Math.min(distance, this.cupertinoLines - distance);
141
+ this.cupertinoOpacity[i] = 1 - (minDistance / this.cupertinoLines) * 0.8;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Dessine le spinner Material
147
+ * @private
148
+ */
149
+ drawMaterial(ctx, centerX, centerY, radius) {
150
+ if (this.indeterminate) {
151
+ // Track (cercle de base - très léger)
152
+ ctx.strokeStyle = 'rgba(98, 0, 238, 0.1)';
153
+ ctx.lineWidth = this.lineWidth;
154
+ ctx.beginPath();
155
+ ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
156
+ ctx.stroke();
157
+
158
+ // Arc animé qui tourne
159
+ ctx.save();
160
+ ctx.translate(centerX, centerY);
161
+ ctx.rotate(this.rotation);
162
+
163
+ ctx.strokeStyle = this.color;
164
+ ctx.lineCap = 'round';
165
+ ctx.lineWidth = this.lineWidth;
166
+ ctx.beginPath();
167
+ ctx.arc(0, 0, radius, this.arcStart, this.arcEnd);
168
+ ctx.stroke();
169
+
170
+ ctx.restore();
171
+ } else {
172
+ // Progress circulaire déterminé
173
+ // Track
174
+ ctx.strokeStyle = '#E0E0E0';
175
+ ctx.lineWidth = this.lineWidth;
176
+ ctx.beginPath();
177
+ ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
178
+ ctx.stroke();
179
+
180
+ // Progress
181
+ const angle = (this.progress / 100) * Math.PI * 2;
182
+ ctx.strokeStyle = this.color;
183
+ ctx.lineCap = 'round';
184
+ ctx.beginPath();
185
+ ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + angle);
186
+ ctx.stroke();
187
+
188
+ // Pourcentage au centre
189
+ if (this.progress > 0) {
190
+ ctx.fillStyle = this.color;
191
+ ctx.font = `bold ${this.size / 3}px -apple-system, sans-serif`;
192
+ ctx.textAlign = 'center';
193
+ ctx.textBaseline = 'middle';
194
+ ctx.fillText(`${Math.round(this.progress)}%`, centerX, centerY);
195
+ }
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Dessine le spinner Cupertino (style iOS)
201
+ * @private
202
+ */
203
+ drawCupertino(ctx, centerX, centerY, radius) {
204
+ if (this.indeterminate) {
205
+ // Spinner iOS avec 12 traits
206
+ ctx.lineCap = 'round';
207
+ ctx.lineWidth = this.lineWidth;
208
+
209
+ for (let i = 0; i < this.cupertinoLines; i++) {
210
+ const angle = (i / this.cupertinoLines) * Math.PI * 2;
211
+ const opacity = this.cupertinoOpacity[i];
212
+
213
+ ctx.save();
214
+ ctx.translate(centerX, centerY);
215
+ ctx.rotate(angle);
216
+
217
+ // Trait avec opacité variable
218
+ const startRadius = radius * 0.6;
219
+ const endRadius = radius;
220
+
221
+ ctx.strokeStyle = this.hexToRgba(this.color, opacity);
222
+ ctx.beginPath();
223
+ ctx.moveTo(0, -startRadius);
224
+ ctx.lineTo(0, -endRadius);
225
+ ctx.stroke();
226
+
227
+ ctx.restore();
228
+ }
229
+ } else {
230
+ // Progress circulaire iOS (plus fin et élégant)
231
+ // Track
232
+ ctx.strokeStyle = '#E5E5EA';
233
+ ctx.lineWidth = this.lineWidth;
234
+ ctx.beginPath();
235
+ ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
236
+ ctx.stroke();
237
+
238
+ // Progress
239
+ const angle = (this.progress / 100) * Math.PI * 2;
240
+ ctx.strokeStyle = this.color;
241
+ ctx.lineCap = 'round';
242
+ ctx.beginPath();
243
+ ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + angle);
244
+ ctx.stroke();
245
+
246
+ // Pas de texte au centre pour iOS (plus minimaliste)
247
+ // Mais si tu veux, décommente :
248
+ /*
249
+ if (this.progress > 0) {
250
+ ctx.fillStyle = this.color;
251
+ ctx.font = `${this.size / 4}px -apple-system, sans-serif`;
252
+ ctx.textAlign = 'center';
253
+ ctx.textBaseline = 'middle';
254
+ ctx.fillText(`${Math.round(this.progress)}%`, centerX, centerY);
255
+ }
256
+ */
257
+ }
258
+ }
259
+
260
+ /**
261
+ * Dessine le spinner
262
+ * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
263
+ */
264
+ draw(ctx) {
265
+ ctx.save();
266
+
267
+ const centerX = this.x + this.size / 2;
268
+ const centerY = this.y + this.size / 2;
269
+ const radius = (this.size - this.lineWidth * 2) / 2;
270
+
271
+ if (this.platform === 'material') {
272
+ this.drawMaterial(ctx, centerX, centerY, radius);
273
+ } else {
274
+ this.drawCupertino(ctx, centerX, centerY, radius);
275
+ }
276
+
277
+ ctx.restore();
278
+ }
279
+
280
+ /**
281
+ * Convertit une couleur hex en rgba avec opacité
282
+ * @private
283
+ */
284
+ hexToRgba(hex, alpha) {
285
+ // Si c'est déjà rgba, le retourner
286
+ if (hex.startsWith('rgba')) return hex;
287
+ if (hex.startsWith('rgb')) {
288
+ return hex.replace('rgb', 'rgba').replace(')', `, ${alpha})`);
289
+ }
290
+
291
+ // Convertir hex en rgba
292
+ const r = parseInt(hex.slice(1, 3), 16);
293
+ const g = parseInt(hex.slice(3, 5), 16);
294
+ const b = parseInt(hex.slice(5, 7), 16);
295
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
296
+ }
297
+
298
+ /**
299
+ * Définit la progression
300
+ * @param {number} value - Valeur de progression (0-100)
301
+ */
302
+ setProgress(value) {
303
+ this.progress = Math.max(0, Math.min(100, value));
304
+ this.indeterminate = false;
305
+ this.markDirty();
306
+ }
307
+
308
+ /**
309
+ * Active le mode indéterminé
310
+ */
311
+ setIndeterminate() {
312
+ this.indeterminate = true;
313
+ if (!this._animating) {
314
+ this.startAnimation();
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Vérifie si un point est dans les limites
320
+ * @returns {boolean} False (non cliquable)
321
+ */
322
+ isPointInside() {
323
+ return false; // Non cliquable
324
+ }
325
+ }
326
+
327
+ export default CircularProgress;