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,17 +1,9 @@
1
1
  import Component from '../core/Component.js';
2
+
2
3
  /**
3
- * Barre d'application supérieure
4
+ * Barre d'application supérieure (Material & Cupertino)
4
5
  * @class
5
6
  * @extends Component
6
- * @property {string} title - Titre
7
- * @property {string|null} leftIcon - Icône gauche ('menu' ou 'back')
8
- * @property {string|null} rightIcon - Icône droite ('search' ou 'more')
9
- * @property {Function} onLeftClick - Callback au clic gauche
10
- * @property {Function} onRightClick - Callback au clic droit
11
- * @property {string} platform - Plateforme
12
- * @property {string} bgColor - Couleur de fond
13
- * @property {string} textColor - Couleur du texte
14
- * @property {number} elevation - Élévation (ombre)
15
7
  */
16
8
  class AppBar extends Component {
17
9
  /**
@@ -19,13 +11,13 @@ class AppBar extends Component {
19
11
  * @param {CanvasFramework} framework - Framework parent
20
12
  * @param {Object} [options={}] - Options de configuration
21
13
  * @param {string} [options.title=''] - Titre
22
- * @param {string} [options.leftIcon] - Icône gauche
23
- * @param {string} [options.rightIcon] - Icône droite
14
+ * @param {string} [options.leftIcon] - Icône gauche ('menu' ou 'back')
15
+ * @param {string} [options.rightIcon] - Icône droite ('search' ou 'more')
24
16
  * @param {Function} [options.onLeftClick] - Callback gauche
25
17
  * @param {Function} [options.onRightClick] - Callback droit
26
18
  * @param {number} [options.height] - Hauteur (auto selon platform)
27
- * @param {string} [options.bgColor] - Couleur de fond (auto selon platform)
28
- * @param {string} [options.textColor] - Couleur du texte (auto selon platform)
19
+ * @param {string} [options.bgColor] - Couleur de fond
20
+ * @param {string} [options.textColor] - Couleur du texte
29
21
  * @param {number} [options.elevation=4] - Élévation (Material)
30
22
  */
31
23
  constructor(framework, options = {}) {
@@ -36,17 +28,31 @@ class AppBar extends Component {
36
28
  height: options.height || (framework.platform === 'material' ? 56 : 44),
37
29
  ...options
38
30
  });
31
+
39
32
  this.title = options.title || '';
40
33
  this.leftIcon = options.leftIcon || null;
41
34
  this.rightIcon = options.rightIcon || null;
42
35
  this.onLeftClick = options.onLeftClick;
43
36
  this.onRightClick = options.onRightClick;
44
37
  this.platform = framework.platform;
45
- this.bgColor = options.bgColor || (framework.platform === 'material' ? '#6200EE' : '#F8F8F8');
46
- this.textColor = options.textColor || (framework.platform === 'material' ? '#FFFFFF' : '#000000');
47
- this.elevation = options.elevation !== undefined ? options.elevation : 4;
48
38
 
49
- // IMPORTANT: Définir onPress pour que le framework appelle handlePress
39
+ // Couleurs selon la plateforme
40
+ if (this.platform === 'material') {
41
+ this.bgColor = options.bgColor || '#6200EE';
42
+ this.textColor = options.textColor || '#FFFFFF';
43
+ this.elevation = options.elevation !== undefined ? options.elevation : 4;
44
+ } else {
45
+ // iOS : Transparent ou blanc avec blur effect (simulé)
46
+ this.bgColor = options.bgColor || 'rgba(248, 248, 248, 0.95)';
47
+ this.textColor = options.textColor || '#000000';
48
+ this.elevation = 0;
49
+ }
50
+
51
+ // Ripple effect (Material uniquement)
52
+ this.ripples = [];
53
+ this.leftPressed = false;
54
+ this.rightPressed = false;
55
+
50
56
  this.onPress = this.handlePress.bind(this);
51
57
  }
52
58
 
@@ -69,50 +75,123 @@ class AppBar extends Component {
69
75
  ctx.fillRect(this.x, this.y, this.width, this.height);
70
76
 
71
77
  ctx.shadowColor = 'transparent';
78
+ ctx.shadowBlur = 0;
79
+ ctx.shadowOffsetY = 0;
72
80
 
73
81
  // Bordure inférieure (iOS uniquement)
74
82
  if (this.platform === 'cupertino') {
75
- ctx.strokeStyle = '#C6C6C8';
83
+ ctx.strokeStyle = 'rgba(0, 0, 0, 0.1)';
76
84
  ctx.lineWidth = 0.5;
77
85
  ctx.beginPath();
78
- ctx.moveTo(this.x, this.y + this.height);
79
- ctx.lineTo(this.x + this.width, this.y + this.height);
86
+ ctx.moveTo(this.x, this.y + this.height - 0.5);
87
+ ctx.lineTo(this.x + this.width, this.y + this.height - 0.5);
80
88
  ctx.stroke();
81
89
  }
82
90
 
91
+ // Ripples pour les boutons (Material)
92
+ if (this.platform === 'material') {
93
+ this.drawRipples(ctx);
94
+ }
95
+
96
+ // Overlay pressed pour iOS
97
+ if (this.platform === 'cupertino') {
98
+ if (this.leftPressed && this.leftIcon) {
99
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
100
+ ctx.beginPath();
101
+ ctx.arc(28, this.y + this.height / 2, 20, 0, Math.PI * 2);
102
+ ctx.fill();
103
+ }
104
+ if (this.rightPressed && this.rightIcon) {
105
+ ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
106
+ ctx.beginPath();
107
+ ctx.arc(this.width - 28, this.y + this.height / 2, 20, 0, Math.PI * 2);
108
+ ctx.fill();
109
+ }
110
+ }
111
+
83
112
  // Titre
84
113
  ctx.fillStyle = this.textColor;
114
+ const titleAlign = this.platform === 'material' && this.leftIcon ? 'left' : 'center';
115
+ const titleX = titleAlign === 'left' ? 72 : this.width / 2;
85
116
  ctx.font = `${this.platform === 'material' ? 'bold ' : ''}20px -apple-system, Roboto, sans-serif`;
86
- ctx.textAlign = 'center';
117
+ ctx.textAlign = titleAlign;
87
118
  ctx.textBaseline = 'middle';
88
- ctx.fillText(this.title, this.width / 2, this.y + this.height / 2);
119
+ ctx.fillText(this.title, titleX, this.y + this.height / 2);
89
120
 
90
- // Icône gauche (hamburger menu ou back)
91
- if (this.leftIcon === 'menu') {
92
- this.drawMenuIcon(ctx, 16, this.y + this.height / 2);
93
- } else if (this.leftIcon === 'back') {
94
- this.drawBackIcon(ctx, 16, this.y + this.height / 2);
121
+ // Icône gauche
122
+ if (this.leftIcon) {
123
+ const iconColor = this.platform === 'cupertino' ? '#007AFF' : this.textColor;
124
+ if (this.leftIcon === 'menu') {
125
+ this.drawMenuIcon(ctx, 16, this.y + this.height / 2, iconColor);
126
+ } else if (this.leftIcon === 'back') {
127
+ this.drawBackIcon(ctx, 16, this.y + this.height / 2, iconColor);
128
+ }
95
129
  }
96
130
 
97
131
  // Icône droite
98
- if (this.rightIcon === 'search') {
99
- this.drawSearchIcon(ctx, this.width - 36, this.y + this.height / 2);
100
- } else if (this.rightIcon === 'more') {
101
- this.drawMoreIcon(ctx, this.width - 36, this.y + this.height / 2);
132
+ if (this.rightIcon) {
133
+ const iconColor = this.platform === 'cupertino' ? '#007AFF' : this.textColor;
134
+ if (this.rightIcon === 'search') {
135
+ this.drawSearchIcon(ctx, this.width - 36, this.y + this.height / 2, iconColor);
136
+ } else if (this.rightIcon === 'more') {
137
+ this.drawMoreIcon(ctx, this.width - 36, this.y + this.height / 2, iconColor);
138
+ }
102
139
  }
103
140
 
104
141
  ctx.restore();
105
142
  }
106
143
 
107
144
  /**
108
- * Dessine l'icône menu
109
- * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
110
- * @param {number} x - Position X
111
- * @param {number} y - Position Y
145
+ * Dessine les ripples
146
+ * @private
147
+ */
148
+ drawRipples(ctx) {
149
+ for (let ripple of this.ripples) {
150
+ ctx.save();
151
+ ctx.globalAlpha = ripple.opacity;
152
+ ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
153
+ ctx.beginPath();
154
+ ctx.arc(ripple.x, ripple.y, ripple.radius, 0, Math.PI * 2);
155
+ ctx.fill();
156
+ ctx.restore();
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Anime les effets ripple
112
162
  * @private
113
163
  */
114
- drawMenuIcon(ctx, x, y) {
115
- ctx.strokeStyle = this.textColor;
164
+ animateRipple() {
165
+ const animate = () => {
166
+ let hasActiveRipples = false;
167
+
168
+ for (let ripple of this.ripples) {
169
+ if (ripple.radius < ripple.maxRadius) {
170
+ ripple.radius += ripple.maxRadius / 15;
171
+ hasActiveRipples = true;
172
+ }
173
+
174
+ if (ripple.radius >= ripple.maxRadius * 0.5) {
175
+ ripple.opacity -= 0.05;
176
+ }
177
+ }
178
+
179
+ this.ripples = this.ripples.filter(r => r.opacity > 0);
180
+
181
+ if (hasActiveRipples) {
182
+ requestAnimationFrame(animate);
183
+ }
184
+ };
185
+
186
+ animate();
187
+ }
188
+
189
+ /**
190
+ * Dessine l'icône menu (hamburger)
191
+ * @private
192
+ */
193
+ drawMenuIcon(ctx, x, y, color) {
194
+ ctx.strokeStyle = color;
116
195
  ctx.lineWidth = 2;
117
196
  ctx.lineCap = 'round';
118
197
  for (let i = 0; i < 3; i++) {
@@ -125,13 +204,10 @@ class AppBar extends Component {
125
204
 
126
205
  /**
127
206
  * Dessine l'icône retour
128
- * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
129
- * @param {number} x - Position X
130
- * @param {number} y - Position Y
131
207
  * @private
132
208
  */
133
- drawBackIcon(ctx, x, y) {
134
- ctx.strokeStyle = this.textColor;
209
+ drawBackIcon(ctx, x, y, color) {
210
+ ctx.strokeStyle = color;
135
211
  ctx.lineWidth = 2;
136
212
  ctx.lineCap = 'round';
137
213
  ctx.lineJoin = 'round';
@@ -144,14 +220,12 @@ class AppBar extends Component {
144
220
 
145
221
  /**
146
222
  * Dessine l'icône recherche
147
- * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
148
- * @param {number} x - Position X
149
- * @param {number} y - Position Y
150
223
  * @private
151
224
  */
152
- drawSearchIcon(ctx, x, y) {
153
- ctx.strokeStyle = this.textColor;
225
+ drawSearchIcon(ctx, x, y, color) {
226
+ ctx.strokeStyle = color;
154
227
  ctx.lineWidth = 2;
228
+ ctx.lineCap = 'round';
155
229
  ctx.beginPath();
156
230
  ctx.arc(x + 8, y - 2, 8, 0, Math.PI * 2);
157
231
  ctx.stroke();
@@ -162,58 +236,78 @@ class AppBar extends Component {
162
236
  }
163
237
 
164
238
  /**
165
- * Dessine l'icône plus
166
- * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
167
- * @param {number} x - Position X
168
- * @param {number} y - Position Y
239
+ * Dessine l'icône plus (3 dots)
169
240
  * @private
170
241
  */
171
- drawMoreIcon(ctx, x, y) {
172
- ctx.fillStyle = this.textColor;
242
+ drawMoreIcon(ctx, x, y, color) {
243
+ ctx.fillStyle = color;
244
+ const spacing = this.platform === 'material' ? 10 : 8;
173
245
  for (let i = 0; i < 3; i++) {
174
246
  ctx.beginPath();
175
- ctx.arc(x + 12, y - 10 + i * 10, 2, 0, Math.PI * 2);
247
+ ctx.arc(x + 12, y - spacing + i * spacing, 2, 0, Math.PI * 2);
176
248
  ctx.fill();
177
249
  }
178
250
  }
179
251
 
180
252
  /**
181
253
  * Vérifie si un point est dans les zones cliquables
182
- * @param {number} x - Coordonnée X
183
- * @param {number} y - Coordonnée Y
184
- * @returns {boolean} True si le point est dans une zone cliquable
185
254
  */
186
255
  isPointInside(x, y) {
187
- // Zones cliquables pour les icônes
188
256
  if (y >= this.y && y <= this.y + this.height) {
189
- if (this.leftIcon && x >= 0 && x <= 56) {
190
- return true;
191
- }
192
- if (this.rightIcon && x >= this.width - 56 && x <= this.width) {
193
- return true;
194
- }
257
+ if (this.leftIcon && x >= 0 && x <= 56) return true;
258
+ if (this.rightIcon && x >= this.width - 56 && x <= this.width) return true;
195
259
  }
196
260
  return false;
197
261
  }
198
262
 
199
263
  /**
200
264
  * Gère la pression (clic)
201
- * @param {number} x - Coordonnée X
202
- * @param {number} y - Coordonnée Y
203
- * @returns {boolean} True si un clic a été traité
204
265
  * @private
205
266
  */
206
267
  handlePress(x, y) {
207
- // Ajuster y avec le scrollOffset si nécessaire
208
268
  const adjustedY = y;
209
269
 
210
- // Détecter quelle zone a été cliquée
211
270
  if (adjustedY >= this.y && adjustedY <= this.y + this.height) {
271
+ // Bouton gauche
212
272
  if (this.leftIcon && x >= 0 && x <= 56) {
273
+ // Ripple effect (Material)
274
+ if (this.platform === 'material') {
275
+ this.ripples.push({
276
+ x: 28,
277
+ y: this.y + this.height / 2,
278
+ radius: 0,
279
+ maxRadius: 28,
280
+ opacity: 1
281
+ });
282
+ this.animateRipple();
283
+ } else {
284
+ // iOS pressed state
285
+ this.leftPressed = true;
286
+ setTimeout(() => { this.leftPressed = false; }, 150);
287
+ }
288
+
213
289
  if (this.onLeftClick) this.onLeftClick();
214
290
  return true;
215
291
  }
292
+
293
+ // Bouton droit
216
294
  if (this.rightIcon && x >= this.width - 56 && x <= this.width) {
295
+ // Ripple effect (Material)
296
+ if (this.platform === 'material') {
297
+ this.ripples.push({
298
+ x: this.width - 28,
299
+ y: this.y + this.height / 2,
300
+ radius: 0,
301
+ maxRadius: 28,
302
+ opacity: 1
303
+ });
304
+ this.animateRipple();
305
+ } else {
306
+ // iOS pressed state
307
+ this.rightPressed = true;
308
+ setTimeout(() => { this.rightPressed = false; }, 150);
309
+ }
310
+
217
311
  if (this.onRightClick) this.onRightClick();
218
312
  return true;
219
313
  }