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.
- package/README.md +554 -0
- package/components/Accordion.js +252 -0
- package/components/AndroidDatePickerDialog.js +398 -0
- package/components/AppBar.js +225 -0
- package/components/Avatar.js +202 -0
- package/components/BottomNavigationBar.js +205 -0
- package/components/BottomSheet.js +374 -0
- package/components/Button.js +225 -0
- package/components/Card.js +193 -0
- package/components/Checkbox.js +180 -0
- package/components/Chip.js +212 -0
- package/components/CircularProgress.js +143 -0
- package/components/ContextMenu.js +116 -0
- package/components/DatePicker.js +257 -0
- package/components/Dialog.js +367 -0
- package/components/Divider.js +125 -0
- package/components/Drawer.js +261 -0
- package/components/FAB.js +270 -0
- package/components/FileUpload.js +315 -0
- package/components/IOSDatePickerWheel.js +268 -0
- package/components/ImageCarousel.js +193 -0
- package/components/ImageComponent.js +223 -0
- package/components/Input.js +309 -0
- package/components/List.js +94 -0
- package/components/ListItem.js +223 -0
- package/components/Modal.js +364 -0
- package/components/MultiSelectDialog.js +206 -0
- package/components/NumberInput.js +271 -0
- package/components/ProgressBar.js +88 -0
- package/components/RadioButton.js +142 -0
- package/components/SearchInput.js +315 -0
- package/components/SegmentedControl.js +202 -0
- package/components/Select.js +199 -0
- package/components/SelectDialog.js +255 -0
- package/components/Slider.js +113 -0
- package/components/Snackbar.js +243 -0
- package/components/Stepper.js +281 -0
- package/components/SwipeableListItem.js +179 -0
- package/components/Switch.js +147 -0
- package/components/Table.js +492 -0
- package/components/Tabs.js +125 -0
- package/components/Text.js +141 -0
- package/components/TextField.js +331 -0
- package/components/Toast.js +236 -0
- package/components/TreeView.js +420 -0
- package/components/Video.js +397 -0
- package/components/View.js +140 -0
- package/components/VirtualList.js +120 -0
- package/core/CanvasFramework.js +1271 -0
- package/core/CanvasWork.js +32 -0
- package/core/Component.js +153 -0
- package/core/LogicWorker.js +25 -0
- package/core/WebGLCanvasAdapter.js +1369 -0
- package/features/Column.js +43 -0
- package/features/Grid.js +47 -0
- package/features/LayoutComponent.js +43 -0
- package/features/OpenStreetMap.js +310 -0
- package/features/Positioned.js +33 -0
- package/features/PullToRefresh.js +328 -0
- package/features/Row.js +40 -0
- package/features/SignaturePad.js +257 -0
- package/features/Skeleton.js +84 -0
- package/features/Stack.js +21 -0
- package/index.js +101 -0
- package/manager/AccessibilityManager.js +107 -0
- package/manager/ErrorHandler.js +59 -0
- package/manager/FeatureFlags.js +60 -0
- package/manager/MemoryManager.js +107 -0
- package/manager/PerformanceMonitor.js +84 -0
- package/manager/SecurityManager.js +54 -0
- package/package.json +28 -0
- package/utils/AnimationEngine.js +428 -0
- package/utils/DataStore.js +403 -0
- package/utils/EventBus.js +407 -0
- package/utils/FetchClient.js +74 -0
- package/utils/FormValidator.js +355 -0
- package/utils/GeoLocationService.js +62 -0
- package/utils/I18n.js +207 -0
- package/utils/IndexedDBManager.js +273 -0
- package/utils/OfflineSyncManager.js +342 -0
- package/utils/QueryBuilder.js +478 -0
- package/utils/SafeArea.js +64 -0
- package/utils/SecureStorage.js +289 -0
- package/utils/StateManager.js +207 -0
- package/utils/WebSocketClient.js +66 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tiroir latéral (navigation)
|
|
5
|
+
* @class
|
|
6
|
+
* @extends Component
|
|
7
|
+
* @property {number} targetX - Position X cible
|
|
8
|
+
* @property {Array} items - Items du drawer
|
|
9
|
+
* @property {Object|null} header - En-tête
|
|
10
|
+
* @property {Function} onItemClick - Callback au clic sur item
|
|
11
|
+
* @property {string} platform - Plateforme
|
|
12
|
+
* @property {boolean} animating - En cours d'animation
|
|
13
|
+
* @property {number} hoveredIndex - Index survolé
|
|
14
|
+
*/
|
|
15
|
+
class Drawer extends Component {
|
|
16
|
+
/**
|
|
17
|
+
* Crée une instance de Drawer
|
|
18
|
+
* @param {CanvasFramework} framework - Framework parent
|
|
19
|
+
* @param {Object} [options={}] - Options de configuration
|
|
20
|
+
* @param {Array} [options.items=[]] - Items [{label, icon, divider}]
|
|
21
|
+
* @param {Object} [options.header] - En-tête {title}
|
|
22
|
+
* @param {Function} [options.onItemClick] - Callback au clic sur item
|
|
23
|
+
*/
|
|
24
|
+
constructor(framework, options = {}) {
|
|
25
|
+
super(framework, {
|
|
26
|
+
x: -framework.width * 0.8,
|
|
27
|
+
y: 0,
|
|
28
|
+
width: framework.width * 0.8,
|
|
29
|
+
height: framework.height,
|
|
30
|
+
visible: false,
|
|
31
|
+
...options
|
|
32
|
+
});
|
|
33
|
+
this.targetX = -this.width;
|
|
34
|
+
this.items = options.items || [];
|
|
35
|
+
this.header = options.header || null;
|
|
36
|
+
this.onItemClick = options.onItemClick;
|
|
37
|
+
this.platform = framework.platform;
|
|
38
|
+
this.animating = false;
|
|
39
|
+
this.hoveredIndex = -1;
|
|
40
|
+
|
|
41
|
+
// Bind des méthodes
|
|
42
|
+
this.handlePress = this.handlePress.bind(this);
|
|
43
|
+
this.handleMove = this.handleMove.bind(this);
|
|
44
|
+
|
|
45
|
+
// IMPORTANT: Définir les callbacks
|
|
46
|
+
this.onPress = this.handlePress;
|
|
47
|
+
this.onMove = this.handleMove;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Ouvre le drawer
|
|
52
|
+
*/
|
|
53
|
+
open() {
|
|
54
|
+
this.visible = true;
|
|
55
|
+
this.targetX = 0;
|
|
56
|
+
this.animate();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Ferme le drawer
|
|
61
|
+
*/
|
|
62
|
+
close() {
|
|
63
|
+
this.targetX = -this.width;
|
|
64
|
+
this.animate();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Anime le drawer
|
|
69
|
+
* @private
|
|
70
|
+
*/
|
|
71
|
+
animate() {
|
|
72
|
+
if (this.animating) return;
|
|
73
|
+
this.animating = true;
|
|
74
|
+
|
|
75
|
+
const step = () => {
|
|
76
|
+
const diff = this.targetX - this.x;
|
|
77
|
+
if (Math.abs(diff) < 1) {
|
|
78
|
+
this.x = this.targetX;
|
|
79
|
+
this.animating = false;
|
|
80
|
+
if (this.targetX < 0) {
|
|
81
|
+
this.visible = false;
|
|
82
|
+
}
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
this.x += diff * 0.2;
|
|
86
|
+
requestAnimationFrame(step);
|
|
87
|
+
};
|
|
88
|
+
step();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Vérifie dans quelle zone se trouve un point
|
|
93
|
+
* @param {number} x - Coordonnée X
|
|
94
|
+
* @param {number} y - Coordonnée Y
|
|
95
|
+
* @returns {string|null} Zone ('overlay', 'item', 'drawer', null si en dehors)
|
|
96
|
+
* @private
|
|
97
|
+
*/
|
|
98
|
+
getZoneAtPoint(x, y) {
|
|
99
|
+
if (!this.visible) return null;
|
|
100
|
+
|
|
101
|
+
// Vérifier si le point est dans l'overlay (toute la zone de l'écran)
|
|
102
|
+
// Mais on ne veut pas capturer les clics sur le drawer lui-même pour les items
|
|
103
|
+
if (x >= this.x && x <= this.x + this.width) {
|
|
104
|
+
// Le point est dans le drawer
|
|
105
|
+
const startY = this.header ? 150 : 0;
|
|
106
|
+
const index = Math.floor((y - startY) / 56);
|
|
107
|
+
if (index >= 0 && index < this.items.length) {
|
|
108
|
+
const itemY = startY + index * 56;
|
|
109
|
+
if (y >= itemY && y <= itemY + 56) {
|
|
110
|
+
return 'item';
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return 'drawer';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Le point est dans l'overlay (zone sombre autour du drawer)
|
|
117
|
+
return 'overlay';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Gère la pression (clic)
|
|
122
|
+
* @param {number} x - Coordonnée X
|
|
123
|
+
* @param {number} y - Coordonnée Y
|
|
124
|
+
* @private
|
|
125
|
+
*/
|
|
126
|
+
handlePress(x, y) {
|
|
127
|
+
const zone = this.getZoneAtPoint(x, y);
|
|
128
|
+
|
|
129
|
+
if (zone === 'overlay') {
|
|
130
|
+
// Clic sur l'overlay - fermer le drawer
|
|
131
|
+
this.close();
|
|
132
|
+
return true; // On a géré le clic
|
|
133
|
+
} else if (zone === 'item') {
|
|
134
|
+
// Clic sur un item
|
|
135
|
+
const startY = this.header ? 150 : 0;
|
|
136
|
+
const index = Math.floor((y - startY) / 56);
|
|
137
|
+
if (index >= 0 && index < this.items.length) {
|
|
138
|
+
if (this.onItemClick) {
|
|
139
|
+
this.onItemClick(index, this.items[index]);
|
|
140
|
+
}
|
|
141
|
+
this.close();
|
|
142
|
+
}
|
|
143
|
+
return true; // On a géré le clic
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Clic sur le drawer (mais pas sur un item) - on ne fait rien mais on capture le clic
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Gère le mouvement (hover)
|
|
152
|
+
* @param {number} x - Coordonnée X
|
|
153
|
+
* @param {number} y - Coordonnée Y
|
|
154
|
+
* @private
|
|
155
|
+
*/
|
|
156
|
+
handleMove(x, y) {
|
|
157
|
+
if (!this.visible) return;
|
|
158
|
+
|
|
159
|
+
const zone = this.getZoneAtPoint(x, y);
|
|
160
|
+
if (zone === 'item') {
|
|
161
|
+
const startY = this.header ? 150 : 0;
|
|
162
|
+
const index = Math.floor((y - startY) / 56);
|
|
163
|
+
this.hoveredIndex = index;
|
|
164
|
+
} else {
|
|
165
|
+
this.hoveredIndex = -1;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Vérifie si un point est dans les limites du drawer (inclut l'overlay)
|
|
171
|
+
* @param {number} x - Coordonnée X
|
|
172
|
+
* @param {number} y - Coordonnée Y
|
|
173
|
+
* @returns {boolean} True si le point est dans le drawer ou l'overlay
|
|
174
|
+
*/
|
|
175
|
+
isPointInside(x, y) {
|
|
176
|
+
if (!this.visible) return false;
|
|
177
|
+
|
|
178
|
+
// Quand le drawer est ouvert, il capture TOUS les clics sur l'écran
|
|
179
|
+
// car il a un overlay qui couvre tout
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Dessine le drawer
|
|
185
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
186
|
+
*/
|
|
187
|
+
draw(ctx) {
|
|
188
|
+
if (!this.visible) return;
|
|
189
|
+
|
|
190
|
+
ctx.save();
|
|
191
|
+
|
|
192
|
+
// Overlay sombre avec opacité progressive
|
|
193
|
+
const overlayOpacity = Math.min(0.5, (this.x + this.width) / this.width * 0.5);
|
|
194
|
+
ctx.fillStyle = `rgba(0, 0, 0, ${overlayOpacity})`;
|
|
195
|
+
ctx.fillRect(0, 0, this.framework.width, this.framework.height);
|
|
196
|
+
|
|
197
|
+
// Drawer
|
|
198
|
+
ctx.fillStyle = '#FFFFFF';
|
|
199
|
+
ctx.fillRect(this.x, this.y, this.width, this.height);
|
|
200
|
+
|
|
201
|
+
// Ombre droite
|
|
202
|
+
const gradient = ctx.createLinearGradient(this.x + this.width, 0, this.x + this.width + 10, 0);
|
|
203
|
+
gradient.addColorStop(0, 'rgba(0, 0, 0, 0.2)');
|
|
204
|
+
gradient.addColorStop(1, 'rgba(0, 0, 0, 0)');
|
|
205
|
+
ctx.fillStyle = gradient;
|
|
206
|
+
ctx.fillRect(this.x + this.width, 0, 10, this.height);
|
|
207
|
+
|
|
208
|
+
// Header
|
|
209
|
+
if (this.header) {
|
|
210
|
+
ctx.fillStyle = this.platform === 'material' ? '#6200EE' : '#F8F8F8';
|
|
211
|
+
ctx.fillRect(this.x, this.y, this.width, 150);
|
|
212
|
+
|
|
213
|
+
ctx.fillStyle = this.platform === 'material' ? '#FFFFFF' : '#000000';
|
|
214
|
+
ctx.font = 'bold 24px -apple-system, Roboto, sans-serif';
|
|
215
|
+
ctx.textAlign = 'left';
|
|
216
|
+
ctx.textBaseline = 'bottom';
|
|
217
|
+
ctx.fillText(this.header.title || '', this.x + 20, this.y + 130);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Items
|
|
221
|
+
const startY = this.header ? 150 : 0;
|
|
222
|
+
for (let i = 0; i < this.items.length; i++) {
|
|
223
|
+
const item = this.items[i];
|
|
224
|
+
const itemY = this.y + startY + i * 56;
|
|
225
|
+
|
|
226
|
+
// Hover effect
|
|
227
|
+
if (this.hoveredIndex === i) {
|
|
228
|
+
ctx.fillStyle = '#F5F5F5';
|
|
229
|
+
ctx.fillRect(this.x, itemY, this.width, 56);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Icon
|
|
233
|
+
if (item.icon) {
|
|
234
|
+
ctx.fillStyle = '#757575';
|
|
235
|
+
ctx.font = '20px -apple-system, Roboto, sans-serif';
|
|
236
|
+
ctx.textAlign = 'left';
|
|
237
|
+
ctx.textBaseline = 'middle';
|
|
238
|
+
ctx.fillText(item.icon, this.x + 20, itemY + 28);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Label
|
|
242
|
+
ctx.fillStyle = '#000000';
|
|
243
|
+
ctx.font = '16px -apple-system, Roboto, sans-serif';
|
|
244
|
+
ctx.fillText(item.label, this.x + (item.icon ? 72 : 20), itemY + 28);
|
|
245
|
+
|
|
246
|
+
// Divider
|
|
247
|
+
if (item.divider) {
|
|
248
|
+
ctx.strokeStyle = '#E0E0E0';
|
|
249
|
+
ctx.lineWidth = 1;
|
|
250
|
+
ctx.beginPath();
|
|
251
|
+
ctx.moveTo(this.x, itemY + 56);
|
|
252
|
+
ctx.lineTo(this.x + this.width, itemY + 56);
|
|
253
|
+
ctx.stroke();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
ctx.restore();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export default Drawer;
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import Component from '../core/Component.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bouton d'action flottant (Material Design 3)
|
|
5
|
+
* @class
|
|
6
|
+
* @extends Component
|
|
7
|
+
* @property {string} icon - Icône du bouton
|
|
8
|
+
* @property {boolean} extended - Mode étendu (avec texte)
|
|
9
|
+
* @property {string} text - Texte (en mode étendu)
|
|
10
|
+
* @property {string} platform - Plateforme
|
|
11
|
+
* @property {string} variant - Variante Material 3: 'small', 'medium', 'large', 'extended'
|
|
12
|
+
* @property {number} size - Taille du bouton
|
|
13
|
+
* @property {string} bgColor - Couleur de fond
|
|
14
|
+
* @property {string} iconColor - Couleur de l'icône
|
|
15
|
+
* @property {Array} ripples - Effets ripple
|
|
16
|
+
*/
|
|
17
|
+
class FAB extends Component {
|
|
18
|
+
/**
|
|
19
|
+
* Crée une instance de FAB
|
|
20
|
+
* @param {CanvasFramework} framework - Framework parent
|
|
21
|
+
* @param {Object} [options={}] - Options de configuration
|
|
22
|
+
* @param {string} [options.icon='+'] - Icône
|
|
23
|
+
* @param {boolean} [options.extended=false] - Mode étendu
|
|
24
|
+
* @param {string} [options.text=''] - Texte (mode étendu)
|
|
25
|
+
* @param {string} [options.variant='medium'] - Variante: 'small', 'medium', 'large', 'extended'
|
|
26
|
+
* @param {string} [options.bgColor] - Couleur (auto selon platform)
|
|
27
|
+
* @param {string} [options.iconColor='#FFFFFF'] - Couleur de l'icône
|
|
28
|
+
*/
|
|
29
|
+
constructor(framework, options = {}) {
|
|
30
|
+
super(framework, options);
|
|
31
|
+
|
|
32
|
+
this.icon = options.icon || '+';
|
|
33
|
+
this.extended = options.extended || false;
|
|
34
|
+
this.text = options.text || '';
|
|
35
|
+
this.platform = framework.platform;
|
|
36
|
+
this.variant = options.variant || 'medium';
|
|
37
|
+
|
|
38
|
+
// Tailles selon Material Design 3
|
|
39
|
+
const sizes = {
|
|
40
|
+
small: 40,
|
|
41
|
+
medium: 56,
|
|
42
|
+
large: 96
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
this.size = options.size || sizes[this.variant] || 56;
|
|
46
|
+
|
|
47
|
+
// Couleurs Material 3
|
|
48
|
+
this.bgColor = options.bgColor || (framework.platform === 'material' ? '#6750A4' : '#007AFF');
|
|
49
|
+
this.iconColor = options.iconColor || '#FFFFFF';
|
|
50
|
+
|
|
51
|
+
// Border radius selon Material 3 (pas circulaire!)
|
|
52
|
+
this.borderRadius = {
|
|
53
|
+
small: 12,
|
|
54
|
+
medium: 16,
|
|
55
|
+
large: 28,
|
|
56
|
+
extended: 16
|
|
57
|
+
}[this.variant] || 16;
|
|
58
|
+
|
|
59
|
+
// Position par défaut en bas à droite
|
|
60
|
+
this.x = options.x !== undefined ? options.x : framework.width - this.size - 16;
|
|
61
|
+
this.y = options.y !== undefined ? options.y : framework.height - this.size - 80;
|
|
62
|
+
|
|
63
|
+
// Si extended, ajuster la largeur
|
|
64
|
+
if (this.extended && this.text) {
|
|
65
|
+
const ctx = framework.ctx;
|
|
66
|
+
ctx.save();
|
|
67
|
+
ctx.font = 'bold 14px -apple-system, sans-serif';
|
|
68
|
+
const textWidth = ctx.measureText(this.text).width;
|
|
69
|
+
ctx.restore();
|
|
70
|
+
this.width = this.size + textWidth + 24;
|
|
71
|
+
this.borderRadius = 16;
|
|
72
|
+
} else {
|
|
73
|
+
this.width = this.size;
|
|
74
|
+
}
|
|
75
|
+
this.height = this.size;
|
|
76
|
+
|
|
77
|
+
// Effet ripple
|
|
78
|
+
this.ripples = [];
|
|
79
|
+
|
|
80
|
+
// ✅ CORRECTION : Binder onPress comme dans Button
|
|
81
|
+
this.onPress = this.handlePress.bind(this);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Gère la pression sur le FAB
|
|
86
|
+
* @param {number} x - Coordonnée X
|
|
87
|
+
* @param {number} y - Coordonnée Y
|
|
88
|
+
* @private
|
|
89
|
+
*/
|
|
90
|
+
handlePress(x, y) {
|
|
91
|
+
// Créer un ripple au point de clic (Material uniquement)
|
|
92
|
+
if (this.platform === 'material') {
|
|
93
|
+
const adjustedY = y - this.framework.scrollOffset;
|
|
94
|
+
this.ripples.push({
|
|
95
|
+
x: x - this.x,
|
|
96
|
+
y: adjustedY - this.y,
|
|
97
|
+
radius: 0,
|
|
98
|
+
maxRadius: Math.max(this.width, this.height) * 1.5,
|
|
99
|
+
opacity: 1
|
|
100
|
+
});
|
|
101
|
+
this.animateRipple();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Anime l'effet ripple
|
|
107
|
+
* @private
|
|
108
|
+
*/
|
|
109
|
+
animateRipple() {
|
|
110
|
+
const animate = () => {
|
|
111
|
+
let hasActiveRipples = false;
|
|
112
|
+
|
|
113
|
+
for (let ripple of this.ripples) {
|
|
114
|
+
if (ripple.radius < ripple.maxRadius) {
|
|
115
|
+
ripple.radius += ripple.maxRadius / 15;
|
|
116
|
+
hasActiveRipples = true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Fade out après 50% de l'expansion
|
|
120
|
+
if (ripple.radius >= ripple.maxRadius * 0.5) {
|
|
121
|
+
ripple.opacity -= 0.05;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Nettoyer les ripples terminés
|
|
126
|
+
this.ripples = this.ripples.filter(r => r.opacity > 0);
|
|
127
|
+
|
|
128
|
+
if (hasActiveRipples) {
|
|
129
|
+
requestAnimationFrame(animate);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
animate();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Dessine le FAB
|
|
138
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
139
|
+
*/
|
|
140
|
+
draw(ctx) {
|
|
141
|
+
ctx.save();
|
|
142
|
+
|
|
143
|
+
// Ombre (elevation)
|
|
144
|
+
if (!this.pressed) {
|
|
145
|
+
ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
|
|
146
|
+
ctx.shadowBlur = this.platform === 'material' ? 8 : 12;
|
|
147
|
+
ctx.shadowOffsetY = this.platform === 'material' ? 4 : 6;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Background - Material 3: rectangles arrondis, pas cercles!
|
|
151
|
+
ctx.fillStyle = this.pressed ? this.darkenColor(this.bgColor) : this.bgColor;
|
|
152
|
+
ctx.beginPath();
|
|
153
|
+
this.roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
|
|
154
|
+
ctx.fill();
|
|
155
|
+
|
|
156
|
+
ctx.shadowColor = 'transparent';
|
|
157
|
+
ctx.shadowBlur = 0;
|
|
158
|
+
ctx.shadowOffsetY = 0;
|
|
159
|
+
|
|
160
|
+
// Clipping pour les ripples (Material uniquement)
|
|
161
|
+
if (this.platform === 'material') {
|
|
162
|
+
ctx.save();
|
|
163
|
+
ctx.beginPath();
|
|
164
|
+
this.roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
|
|
165
|
+
ctx.clip();
|
|
166
|
+
|
|
167
|
+
// Dessiner les ripples
|
|
168
|
+
for (let ripple of this.ripples) {
|
|
169
|
+
ctx.globalAlpha = ripple.opacity;
|
|
170
|
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
|
|
171
|
+
ctx.beginPath();
|
|
172
|
+
ctx.arc(this.x + ripple.x, this.y + ripple.y, ripple.radius, 0, Math.PI * 2);
|
|
173
|
+
ctx.fill();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
ctx.restore();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Overlay si pressed (iOS)
|
|
180
|
+
if (this.pressed && this.platform === 'cupertino') {
|
|
181
|
+
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
|
|
182
|
+
ctx.beginPath();
|
|
183
|
+
this.roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
|
|
184
|
+
ctx.fill();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Icône
|
|
188
|
+
ctx.fillStyle = this.iconColor;
|
|
189
|
+
const iconSize = this.variant === 'large' ? 36 : 24;
|
|
190
|
+
ctx.font = `bold ${iconSize}px sans-serif`;
|
|
191
|
+
ctx.textAlign = 'center';
|
|
192
|
+
ctx.textBaseline = 'middle';
|
|
193
|
+
|
|
194
|
+
if (this.extended && this.text) {
|
|
195
|
+
// Icône à gauche
|
|
196
|
+
ctx.fillText(this.icon, this.x + this.size / 2, this.y + this.size / 2);
|
|
197
|
+
|
|
198
|
+
// Texte à droite
|
|
199
|
+
ctx.font = 'bold 14px -apple-system, sans-serif';
|
|
200
|
+
ctx.fillText(this.text, this.x + this.size + 12, this.y + this.size / 2);
|
|
201
|
+
} else {
|
|
202
|
+
// Icône centrée
|
|
203
|
+
ctx.fillText(this.icon, this.x + this.width / 2, this.y + this.height / 2);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
ctx.restore();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Dessine un rectangle avec coins arrondis
|
|
211
|
+
* @param {CanvasRenderingContext2D} ctx - Contexte de dessin
|
|
212
|
+
* @param {number} x - Position X
|
|
213
|
+
* @param {number} y - Position Y
|
|
214
|
+
* @param {number} width - Largeur
|
|
215
|
+
* @param {number} height - Hauteur
|
|
216
|
+
* @param {number} radius - Rayon des coins
|
|
217
|
+
* @private
|
|
218
|
+
*/
|
|
219
|
+
roundRect(ctx, x, y, width, height, radius) {
|
|
220
|
+
ctx.moveTo(x + radius, y);
|
|
221
|
+
ctx.lineTo(x + width - radius, y);
|
|
222
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
223
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
224
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
225
|
+
ctx.lineTo(x + radius, y + height);
|
|
226
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
227
|
+
ctx.lineTo(x, y + radius);
|
|
228
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Assombrit une couleur
|
|
233
|
+
* @param {string} color - Couleur hexadécimale
|
|
234
|
+
* @returns {string} Couleur assombrie
|
|
235
|
+
* @private
|
|
236
|
+
*/
|
|
237
|
+
darkenColor(color) {
|
|
238
|
+
const rgb = this.hexToRgb(color);
|
|
239
|
+
return `rgb(${Math.max(0, rgb.r - 30)}, ${Math.max(0, rgb.g - 30)}, ${Math.max(0, rgb.b - 30)})`;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Convertit une couleur hex en RGB
|
|
244
|
+
* @param {string} hex - Couleur hexadécimale
|
|
245
|
+
* @returns {{r: number, g: number, b: number}} Objet RGB
|
|
246
|
+
* @private
|
|
247
|
+
*/
|
|
248
|
+
hexToRgb(hex) {
|
|
249
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
250
|
+
return result ? {
|
|
251
|
+
r: parseInt(result[1], 16),
|
|
252
|
+
g: parseInt(result[2], 16),
|
|
253
|
+
b: parseInt(result[3], 16)
|
|
254
|
+
} : { r: 0, g: 0, b: 0 };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Vérifie si un point est dans les limites
|
|
259
|
+
* @param {number} x - Coordonnée X
|
|
260
|
+
* @param {number} y - Coordonnée Y
|
|
261
|
+
* @returns {boolean} True si le point est dans le FAB
|
|
262
|
+
*/
|
|
263
|
+
isPointInside(x, y) {
|
|
264
|
+
// Material 3: toujours des rectangles arrondis, plus de cercles
|
|
265
|
+
return x >= this.x && x <= this.x + this.width &&
|
|
266
|
+
y >= this.y && y <= this.y + this.height;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export default FAB;
|