canvasframework 0.3.9 → 0.3.10

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.
@@ -1,15 +1,9 @@
1
1
  import Component from '../core/Component.js';
2
+
2
3
  /**
3
- * Barre de navigation inférieure
4
+ * Barre de navigation inférieure (Material & Cupertino)
4
5
  * @class
5
6
  * @extends Component
6
- * @property {Array} items - Items de navigation
7
- * @property {number} selectedIndex - Index sélectionné
8
- * @property {Function} onChange - Callback au changement
9
- * @property {string} platform - Plateforme
10
- * @property {string} bgColor - Couleur de fond
11
- * @property {string} selectedColor - Couleur sélectionnée
12
- * @property {string} unselectedColor - Couleur non sélectionnée
13
7
  */
14
8
  class BottomNavigationBar extends Component {
15
9
  /**
@@ -19,45 +13,106 @@ class BottomNavigationBar extends Component {
19
13
  * @param {Array} [options.items=[]] - Items [{icon, label}]
20
14
  * @param {number} [options.selectedIndex=0] - Index sélectionné
21
15
  * @param {Function} [options.onChange] - Callback au changement
22
- * @param {number} [options.height] - Hauteur (auto selon platform)
23
- * @param {string} [options.bgColor] - Couleur de fond (auto selon platform)
24
- * @param {string} [options.selectedColor] - Couleur sélectionnée (auto selon platform)
25
- * @param {string} [options.unselectedColor='#757575'] - Couleur non sélectionnée
16
+ * @param {number} [options.height] - Hauteur
17
+ * @param {string} [options.bgColor] - Couleur de fond
18
+ * @param {string} [options.selectedColor] - Couleur sélectionnée
19
+ * @param {string} [options.unselectedColor] - Couleur non sélectionnée
26
20
  */
27
21
  constructor(framework, options = {}) {
22
+ const height = options.height || (framework.platform === 'material' ? 56 : 50);
23
+
28
24
  super(framework, {
29
25
  x: 0,
30
- y: framework.height - (options.height || 56),
26
+ y: framework.height - height,
31
27
  width: framework.width,
32
- height: options.height || 56,
28
+ height: height,
33
29
  ...options
34
30
  });
31
+
35
32
  this.items = options.items || [];
36
33
  this.selectedIndex = options.selectedIndex || 0;
37
34
  this.onChange = options.onChange;
38
35
  this.platform = framework.platform;
39
- this.bgColor = options.bgColor || '#FFFFFF';
40
- this.selectedColor = options.selectedColor || (framework.platform === 'material' ? '#6200EE' : '#007AFF');
41
- this.unselectedColor = options.unselectedColor || '#757575';
42
36
 
43
- // IMPORTANT: Définir onPress pour que le framework l'appelle
37
+ // Couleurs selon la plateforme
38
+ if (this.platform === 'material') {
39
+ this.bgColor = options.bgColor || '#FFFFFF';
40
+ this.selectedColor = options.selectedColor || '#6200EE';
41
+ this.unselectedColor = options.unselectedColor || '#757575';
42
+ this.rippleColor = 'rgba(98, 0, 238, 0.2)';
43
+ } else {
44
+ // iOS : background transparent avec blur
45
+ this.bgColor = options.bgColor || 'rgba(248, 248, 248, 0.95)';
46
+ this.selectedColor = options.selectedColor || '#007AFF';
47
+ this.unselectedColor = options.unselectedColor || '#8E8E93';
48
+ }
49
+
50
+ // Ripple effect (Material)
51
+ this.ripples = [];
52
+
53
+ // Animation de l'indicateur (iOS)
54
+ this.indicatorX = 0;
55
+ this.targetIndicatorX = 0;
56
+ this.animatingIndicator = false;
57
+
44
58
  this.onPress = this.handlePress.bind(this);
59
+
60
+ // Initialiser la position de l'indicateur
61
+ this.updateIndicatorPosition();
62
+ }
63
+
64
+ /**
65
+ * Met à jour la position de l'indicateur iOS
66
+ * @private
67
+ */
68
+ updateIndicatorPosition() {
69
+ const itemWidth = this.width / this.items.length;
70
+ this.targetIndicatorX = this.selectedIndex * itemWidth;
71
+
72
+ if (!this.animatingIndicator) {
73
+ this.indicatorX = this.targetIndicatorX;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Anime l'indicateur iOS
79
+ * @private
80
+ */
81
+ animateIndicator() {
82
+ this.animatingIndicator = true;
83
+ const animate = () => {
84
+ const diff = this.targetIndicatorX - this.indicatorX;
85
+ if (Math.abs(diff) > 0.5) {
86
+ this.indicatorX += diff * 0.2;
87
+ requestAnimationFrame(animate);
88
+ } else {
89
+ this.indicatorX = this.targetIndicatorX;
90
+ this.animatingIndicator = false;
91
+ }
92
+ };
93
+ animate();
45
94
  }
46
95
 
47
96
  /**
48
97
  * Dessine la barre de navigation
49
- * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
50
98
  */
51
99
  draw(ctx) {
52
100
  ctx.save();
53
101
 
54
- // Ombre/bordure supérieure
102
+ // Background
103
+ ctx.fillStyle = this.bgColor;
104
+ ctx.fillRect(this.x, this.y, this.width, this.height);
105
+
106
+ // Bordure/Ombre supérieure
55
107
  if (this.platform === 'material') {
56
108
  ctx.shadowColor = 'rgba(0, 0, 0, 0.1)';
57
109
  ctx.shadowBlur = 8;
58
110
  ctx.shadowOffsetY = -2;
111
+ ctx.fillRect(this.x, this.y, this.width, 1);
112
+ ctx.shadowColor = 'transparent';
59
113
  } else {
60
- ctx.strokeStyle = '#C6C6C8';
114
+ // iOS : fine ligne de séparation
115
+ ctx.strokeStyle = 'rgba(0, 0, 0, 0.1)';
61
116
  ctx.lineWidth = 0.5;
62
117
  ctx.beginPath();
63
118
  ctx.moveTo(this.x, this.y);
@@ -65,136 +120,218 @@ class BottomNavigationBar extends Component {
65
120
  ctx.stroke();
66
121
  }
67
122
 
68
- // Background
69
- ctx.fillStyle = this.bgColor;
70
- ctx.fillRect(this.x, this.y, this.width, this.height);
71
-
72
- ctx.shadowColor = 'transparent';
123
+ // Ripples (Material)
124
+ if (this.platform === 'material') {
125
+ this.drawRipples(ctx);
126
+ }
73
127
 
74
128
  // Items
75
129
  const itemWidth = this.width / this.items.length;
130
+
76
131
  for (let i = 0; i < this.items.length; i++) {
77
132
  const item = this.items[i];
78
133
  const itemX = this.x + i * itemWidth;
79
134
  const isSelected = i === this.selectedIndex;
80
135
  const color = isSelected ? this.selectedColor : this.unselectedColor;
81
136
 
137
+ // iOS : Indicateur de sélection (fond arrondi)
138
+ if (this.platform === 'cupertino' && isSelected) {
139
+ ctx.fillStyle = `${this.selectedColor}15`;
140
+ const indicatorWidth = 60;
141
+ const indicatorHeight = 32;
142
+ const indicatorX = itemX + itemWidth / 2 - indicatorWidth / 2;
143
+ const indicatorY = this.y + 6;
144
+
145
+ ctx.beginPath();
146
+ this.roundRect(ctx, indicatorX, indicatorY, indicatorWidth, indicatorHeight, 16);
147
+ ctx.fill();
148
+ }
149
+
82
150
  // Icône
83
- this.drawIcon(ctx, item.icon, itemX + itemWidth / 2, this.y + 16, color);
151
+ const iconY = this.platform === 'material' ? this.y + 12 : this.y + 8;
152
+ this.drawIcon(ctx, item.icon, itemX + itemWidth / 2, iconY, color, isSelected);
84
153
 
85
154
  // Label
86
155
  ctx.fillStyle = color;
87
- ctx.font = `${isSelected ? 'bold ' : ''}12px -apple-system, Roboto, sans-serif`;
156
+ const fontSize = this.platform === 'material' ? 12 : 10;
157
+ ctx.font = `${isSelected && this.platform === 'material' ? 'bold ' : ''}${fontSize}px -apple-system, Roboto, sans-serif`;
88
158
  ctx.textAlign = 'center';
89
159
  ctx.textBaseline = 'top';
90
- ctx.fillText(item.label, itemX + itemWidth / 2, this.y + 36);
160
+ const labelY = this.platform === 'material' ? this.y + 34 : this.y + 30;
161
+ ctx.fillText(item.label, itemX + itemWidth / 2, labelY);
91
162
  }
92
163
 
93
164
  ctx.restore();
94
165
  }
95
166
 
167
+ /**
168
+ * Dessine les ripples (Material)
169
+ * @private
170
+ */
171
+ drawRipples(ctx) {
172
+ for (let ripple of this.ripples) {
173
+ ctx.save();
174
+ ctx.globalAlpha = ripple.opacity;
175
+ ctx.fillStyle = this.rippleColor;
176
+ ctx.beginPath();
177
+ ctx.arc(ripple.x, ripple.y, ripple.radius, 0, Math.PI * 2);
178
+ ctx.fill();
179
+ ctx.restore();
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Anime les effets ripple
185
+ * @private
186
+ */
187
+ animateRipple() {
188
+ const animate = () => {
189
+ let hasActiveRipples = false;
190
+
191
+ for (let ripple of this.ripples) {
192
+ if (ripple.radius < ripple.maxRadius) {
193
+ ripple.radius += ripple.maxRadius / 12;
194
+ hasActiveRipples = true;
195
+ }
196
+
197
+ if (ripple.radius >= ripple.maxRadius * 0.5) {
198
+ ripple.opacity -= 0.05;
199
+ }
200
+ }
201
+
202
+ this.ripples = this.ripples.filter(r => r.opacity > 0);
203
+
204
+ if (hasActiveRipples) {
205
+ requestAnimationFrame(animate);
206
+ }
207
+ };
208
+
209
+ animate();
210
+ }
211
+
96
212
  /**
97
213
  * Dessine une icône
98
- * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
99
- * @param {string} icon - Type d'icône
100
- * @param {number} x - Position X
101
- * @param {number} y - Position Y
102
- * @param {string} color - Couleur
103
214
  * @private
104
215
  */
105
- drawIcon(ctx, icon, x, y, color) {
216
+ drawIcon(ctx, icon, x, y, color, isSelected) {
106
217
  ctx.strokeStyle = color;
107
218
  ctx.fillStyle = color;
108
- ctx.lineWidth = 2;
219
+ ctx.lineWidth = isSelected ? 2.5 : 2;
109
220
  ctx.lineCap = 'round';
110
221
  ctx.lineJoin = 'round';
111
222
 
112
223
  switch(icon) {
113
224
  case 'home':
114
225
  ctx.beginPath();
115
- ctx.moveTo(x, y);
116
- ctx.lineTo(x - 10, y + 8);
117
- ctx.lineTo(x - 10, y + 16);
118
- ctx.lineTo(x + 10, y + 16);
119
- ctx.lineTo(x + 10, y + 8);
226
+ ctx.moveTo(x, y + 2);
227
+ ctx.lineTo(x - 10, y + 10);
228
+ ctx.lineTo(x - 10, y + 18);
229
+ ctx.lineTo(x + 10, y + 18);
230
+ ctx.lineTo(x + 10, y + 10);
120
231
  ctx.closePath();
121
- ctx.stroke();
232
+ if (isSelected) ctx.fill();
233
+ else ctx.stroke();
122
234
  break;
123
235
 
124
236
  case 'search':
125
237
  ctx.beginPath();
126
- ctx.arc(x - 2, y + 4, 6, 0, Math.PI * 2);
238
+ ctx.arc(x - 2, y + 6, 7, 0, Math.PI * 2);
127
239
  ctx.stroke();
128
240
  ctx.beginPath();
129
- ctx.moveTo(x + 3, y + 9);
130
- ctx.lineTo(x + 8, y + 14);
241
+ ctx.moveTo(x + 4, y + 11);
242
+ ctx.lineTo(x + 9, y + 16);
131
243
  ctx.stroke();
132
244
  break;
133
245
 
134
246
  case 'favorite':
135
247
  ctx.beginPath();
136
- ctx.moveTo(x, y + 2);
137
- ctx.lineTo(x + 6, y + 14);
138
- ctx.lineTo(x - 6, y + 14);
139
- ctx.closePath();
140
- ctx.beginPath();
141
- ctx.moveTo(x, y + 14);
142
- ctx.lineTo(x + 10, y + 6);
143
- ctx.lineTo(x + 4, y + 6);
144
- ctx.lineTo(x, y);
145
- ctx.lineTo(x - 4, y + 6);
146
- ctx.lineTo(x - 10, y + 6);
248
+ ctx.moveTo(x, y + 3);
249
+ for (let i = 0; i < 5; i++) {
250
+ const angle = (i * 4 * Math.PI / 5) - Math.PI / 2;
251
+ const radius = i % 2 === 0 ? 9 : 4;
252
+ ctx.lineTo(x + Math.cos(angle) * radius, y + 10 + Math.sin(angle) * radius);
253
+ }
147
254
  ctx.closePath();
148
- ctx.fill();
255
+ if (isSelected) ctx.fill();
256
+ else ctx.stroke();
149
257
  break;
150
258
 
151
259
  case 'person':
152
260
  ctx.beginPath();
153
- ctx.arc(x, y + 4, 5, 0, Math.PI * 2);
261
+ ctx.arc(x, y + 6, 5, 0, Math.PI * 2);
154
262
  ctx.stroke();
155
263
  ctx.beginPath();
156
- ctx.arc(x, y + 16, 8, Math.PI, 0, true);
264
+ ctx.arc(x, y + 20, 9, Math.PI, 0, true);
157
265
  ctx.stroke();
158
266
  break;
159
267
 
160
268
  case 'settings':
161
269
  ctx.beginPath();
162
- ctx.arc(x, y + 8, 4, 0, Math.PI * 2);
270
+ ctx.arc(x, y + 10, 5, 0, Math.PI * 2);
163
271
  ctx.stroke();
164
272
  for (let i = 0; i < 4; i++) {
165
273
  const angle = (i * Math.PI / 2) - Math.PI / 4;
166
274
  ctx.beginPath();
167
- ctx.moveTo(x + Math.cos(angle) * 6, y + 8 + Math.sin(angle) * 6);
168
- ctx.lineTo(x + Math.cos(angle) * 10, y + 8 + Math.sin(angle) * 10);
275
+ ctx.moveTo(x + Math.cos(angle) * 7, y + 10 + Math.sin(angle) * 7);
276
+ ctx.lineTo(x + Math.cos(angle) * 11, y + 10 + Math.sin(angle) * 11);
169
277
  ctx.stroke();
170
278
  }
171
279
  break;
172
280
  }
173
281
  }
174
282
 
283
+ /**
284
+ * Dessine un rectangle arrondi
285
+ * @private
286
+ */
287
+ roundRect(ctx, x, y, width, height, radius) {
288
+ ctx.moveTo(x + radius, y);
289
+ ctx.lineTo(x + width - radius, y);
290
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
291
+ ctx.lineTo(x + width, y + height - radius);
292
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
293
+ ctx.lineTo(x + radius, y + height);
294
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
295
+ ctx.lineTo(x, y + radius);
296
+ ctx.quadraticCurveTo(x, y, x + radius, y);
297
+ }
298
+
175
299
  /**
176
300
  * Vérifie si un point est dans les limites
177
- * @param {number} x - Coordonnée X
178
- * @param {number} y - Coordonnée Y
179
- * @returns {boolean} True si le point est dans la barre
180
301
  */
181
302
  isPointInside(x, y) {
182
- return y >= this.y && y <= this.y + this.height;
303
+ return y >= this.y && y <= this.y + this.height;
183
304
  }
184
305
 
185
306
  /**
186
307
  * Gère la pression (clic)
187
- * @param {number} x - Coordonnée X
188
- * @param {number} y - Coordonnée Y
189
308
  * @private
190
309
  */
191
310
  handlePress(x, y) {
192
- // Calculer quel item a été cliqué
193
311
  const itemWidth = this.width / this.items.length;
194
312
  const index = Math.floor(x / itemWidth);
195
313
 
196
314
  if (index >= 0 && index < this.items.length && index !== this.selectedIndex) {
315
+ // Ripple effect (Material)
316
+ if (this.platform === 'material') {
317
+ this.ripples.push({
318
+ x: (index + 0.5) * itemWidth,
319
+ y: this.y + this.height / 2,
320
+ radius: 0,
321
+ maxRadius: itemWidth / 2,
322
+ opacity: 1
323
+ });
324
+ this.animateRipple();
325
+ }
326
+
197
327
  this.selectedIndex = index;
328
+ this.updateIndicatorPosition();
329
+
330
+ // Animer l'indicateur (iOS)
331
+ if (this.platform === 'cupertino') {
332
+ this.animateIndicator();
333
+ }
334
+
198
335
  if (this.onChange) {
199
336
  this.onChange(index, this.items[index]);
200
337
  }