canvasframework 0.5.41 → 0.5.42

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.
@@ -6,159 +6,71 @@ import Component from '../core/Component.js';
6
6
  * @extends Component
7
7
  */
8
8
  class AppBar extends Component {
9
- /**
10
- * Crée une instance de AppBar
11
- * @param {CanvasFramework} framework - Framework parent
12
- * @param {Object} [options={}] - Options de configuration
13
- * @param {string} [options.title=''] - Titre
14
- * @param {string} [options.leftIcon] - Icône gauche ('menu' ou 'back')
15
- * @param {string} [options.rightIcon] - Icône droite ('search' ou 'more')
16
- * @param {Function} [options.onLeftClick] - Callback gauche
17
- * @param {Function} [options.onRightClick] - Callback droit
18
- * @param {number} [options.height] - Hauteur (auto selon platform)
19
- * @param {string} [options.bgColor] - Couleur de fond
20
- * @param {string} [options.textColor] - Couleur du texte
21
- * @param {number} [options.elevation=4] - Élévation (Material)
22
- */
23
9
  constructor(framework, options = {}) {
10
+ // Détection automatique Material ou Cupertino
11
+ const platform = options.platform || (() => {
12
+ if (framework.platform) return framework.platform;
13
+ return /iPad|iPhone|iPod/.test(navigator.userAgent) ? 'cupertino' : 'material';
14
+ })();
15
+
24
16
  super(framework, {
25
17
  x: 0,
26
18
  y: 0,
27
19
  width: framework.width,
28
- height: options.height || (framework.platform === 'material' ? 56 : 44),
20
+ height: options.height || (platform === 'material' ? 56 : 44),
29
21
  ...options
30
22
  });
31
-
23
+
32
24
  this.title = options.title || '';
33
25
  this.leftIcon = options.leftIcon || null;
34
26
  this.rightIcon = options.rightIcon || null;
35
27
  this.onLeftClick = options.onLeftClick;
36
28
  this.onRightClick = options.onRightClick;
37
- this.platform = framework.platform;
38
-
39
- // Couleurs selon la plateforme
29
+ this.platform = platform;
30
+
31
+ // Couleurs et styles par plateforme
40
32
  if (this.platform === 'material') {
41
33
  this.bgColor = options.bgColor || '#6200EE';
42
34
  this.textColor = options.textColor || '#FFFFFF';
43
35
  this.elevation = options.elevation !== undefined ? options.elevation : 4;
44
36
  } else {
45
- // iOS : Transparent ou blanc avec blur effect (simulé)
37
+ // Cupertino (iOS)
46
38
  this.bgColor = options.bgColor || 'rgba(248, 248, 248, 0.95)';
47
39
  this.textColor = options.textColor || '#000000';
48
40
  this.elevation = 0;
49
41
  }
50
-
51
- // Ripple effect (Material uniquement)
42
+
43
+ // Ripples (Material)
52
44
  this.ripples = [];
53
45
  this.animationFrame = null;
54
46
  this.lastAnimationTime = 0;
55
-
56
- // États pressed pour iOS
47
+
48
+ // États press (iOS)
57
49
  this.leftPressed = false;
58
50
  this.rightPressed = false;
59
-
60
- this.onPress = this.handlePress.bind(this);
61
- }
62
-
63
- /**
64
- * Démarrer l'animation des ripples
65
- * @private
66
- */
67
- startRippleAnimation() {
68
- const animate = (timestamp) => {
69
- if (!this.lastAnimationTime) this.lastAnimationTime = timestamp;
70
- const deltaTime = timestamp - this.lastAnimationTime;
71
- this.lastAnimationTime = timestamp;
72
-
73
- let needsUpdate = false;
74
-
75
- // Mettre à jour chaque ripple
76
- for (let i = this.ripples.length - 1; i >= 0; i--) {
77
- const ripple = this.ripples[i];
78
-
79
- // Animer le rayon (expansion)
80
- if (ripple.radius < ripple.maxRadius) {
81
- ripple.radius += (ripple.maxRadius / 250) * deltaTime;
82
- needsUpdate = true;
83
- }
84
-
85
- // Animer l'opacité (fade out) - commencer plus tôt
86
- if (ripple.radius >= ripple.maxRadius * 0.4) {
87
- ripple.opacity -= (0.003 * deltaTime);
88
- if (ripple.opacity < 0) ripple.opacity = 0;
89
- needsUpdate = true;
90
- }
91
-
92
- // Supprimer les ripples terminés
93
- if (ripple.opacity <= 0 && ripple.radius >= ripple.maxRadius * 0.95) {
94
- this.ripples.splice(i, 1);
95
- needsUpdate = true;
96
- }
97
- }
98
-
99
- // Redessiner si nécessaire
100
- if (needsUpdate) {
101
- this.requestRender();
102
- }
103
-
104
- // Continuer l'animation
105
- if (this.ripples.length > 0) {
106
- this.animationFrame = requestAnimationFrame(animate);
107
- } else {
108
- this.animationFrame = null;
109
- this.lastAnimationTime = 0;
110
- }
111
- };
112
-
113
- if (this.ripples.length > 0 && !this.animationFrame) {
114
- this.animationFrame = requestAnimationFrame(animate);
115
- }
116
- }
117
51
 
118
- /**
119
- * Demander un redessin
120
- * @private
121
- */
122
- requestRender() {
123
- if (this.framework && this.framework.requestRender) {
124
- this.framework.requestRender();
125
- }
126
- }
127
-
128
- /**
129
- * Nettoyer l'animation lors de la destruction
130
- */
131
- destroy() {
132
- if (this.animationFrame) {
133
- cancelAnimationFrame(this.animationFrame);
134
- this.animationFrame = null;
135
- }
136
- super.destroy();
52
+ this.onPress = this.handlePress.bind(this);
137
53
  }
138
54
 
139
- /**
140
- * Dessine l'AppBar
141
- * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
142
- */
143
55
  draw(ctx) {
144
56
  ctx.save();
145
-
146
- // Ombre (Material uniquement)
57
+
58
+ // Ombre Material
147
59
  if (this.platform === 'material' && this.elevation > 0) {
148
60
  ctx.shadowColor = 'rgba(0, 0, 0, 0.2)';
149
61
  ctx.shadowBlur = this.elevation * 2;
150
62
  ctx.shadowOffsetY = this.elevation / 2;
151
63
  }
152
-
64
+
153
65
  // Background
154
66
  ctx.fillStyle = this.bgColor;
155
67
  ctx.fillRect(this.x, this.y, this.width, this.height);
156
-
68
+
157
69
  ctx.shadowColor = 'transparent';
158
70
  ctx.shadowBlur = 0;
159
71
  ctx.shadowOffsetY = 0;
160
-
161
- // Bordure inférieure (iOS uniquement)
72
+
73
+ // Bordure iOS
162
74
  if (this.platform === 'cupertino') {
163
75
  ctx.strokeStyle = 'rgba(0, 0, 0, 0.1)';
164
76
  ctx.lineWidth = 0.5;
@@ -167,28 +79,26 @@ class AppBar extends Component {
167
79
  ctx.lineTo(this.x + this.width, this.y + this.height - 0.5);
168
80
  ctx.stroke();
169
81
  }
170
-
171
- // Ripples pour les boutons (Material)
172
- if (this.platform === 'material') {
173
- this.drawRipples(ctx);
174
- }
175
-
176
- // Overlay pressed pour iOS
82
+
83
+ // Ripples pour Material
84
+ if (this.platform === 'material') this.drawRipples(ctx);
85
+
86
+ // Overlay pressed iOS
177
87
  if (this.platform === 'cupertino') {
178
88
  if (this.leftPressed && this.leftIcon) {
179
- ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
89
+ ctx.fillStyle = 'rgba(0,0,0,0.1)';
180
90
  ctx.beginPath();
181
91
  ctx.arc(this.x + 28, this.y + this.height / 2, 20, 0, Math.PI * 2);
182
92
  ctx.fill();
183
93
  }
184
94
  if (this.rightPressed && this.rightIcon) {
185
- ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
95
+ ctx.fillStyle = 'rgba(0,0,0,0.1)';
186
96
  ctx.beginPath();
187
97
  ctx.arc(this.x + this.width - 28, this.y + this.height / 2, 20, 0, Math.PI * 2);
188
98
  ctx.fill();
189
99
  }
190
100
  }
191
-
101
+
192
102
  // Titre
193
103
  ctx.fillStyle = this.textColor;
194
104
  const titleAlign = this.platform === 'material' && this.leftIcon ? 'left' : 'center';
@@ -197,34 +107,14 @@ class AppBar extends Component {
197
107
  ctx.textAlign = titleAlign;
198
108
  ctx.textBaseline = 'middle';
199
109
  ctx.fillText(this.title, titleX, this.y + this.height / 2);
200
-
201
- // Icône gauche
202
- if (this.leftIcon) {
203
- const iconColor = this.platform === 'cupertino' ? this.textColor : this.textColor;
204
- if (this.leftIcon === 'menu') {
205
- this.drawMenuIcon(ctx, this.x + 16, this.y + this.height / 2, iconColor);
206
- } else if (this.leftIcon === 'back') {
207
- this.drawBackIcon(ctx, this.x + 16, this.y + this.height / 2, iconColor);
208
- }
209
- }
210
-
211
- // Icône droite
212
- if (this.rightIcon) {
213
- const iconColor = this.platform === 'cupertino' ? '#007AFF' : this.textColor;
214
- if (this.rightIcon === 'search') {
215
- this.drawSearchIcon(ctx, this.x + this.width - 36, this.y + this.height / 2, iconColor);
216
- } else if (this.rightIcon === 'more') {
217
- this.drawMoreIcon(ctx, this.x + this.width - 36, this.y + this.height / 2, iconColor);
218
- }
219
- }
220
-
110
+
111
+ // Icônes
112
+ if (this.leftIcon) this.drawLeftIcon(ctx);
113
+ if (this.rightIcon) this.drawRightIcon(ctx);
114
+
221
115
  ctx.restore();
222
116
  }
223
117
 
224
- /**
225
- * Dessine les ripples
226
- * @private
227
- */
228
118
  drawRipples(ctx) {
229
119
  for (let ripple of this.ripples) {
230
120
  ctx.save();
@@ -237,10 +127,18 @@ class AppBar extends Component {
237
127
  }
238
128
  }
239
129
 
240
- /**
241
- * Dessine l'icône menu (hamburger)
242
- * @private
243
- */
130
+ drawLeftIcon(ctx) {
131
+ const color = this.platform === 'cupertino' ? this.textColor : this.textColor;
132
+ if (this.leftIcon === 'menu') this.drawMenuIcon(ctx, this.x + 16, this.y + this.height / 2, color);
133
+ if (this.leftIcon === 'back') this.drawBackIcon(ctx, this.x + 16, this.y + this.height / 2, color);
134
+ }
135
+
136
+ drawRightIcon(ctx) {
137
+ const color = this.platform === 'cupertino' ? '#007AFF' : this.textColor;
138
+ if (this.rightIcon === 'search') this.drawSearchIcon(ctx, this.x + this.width - 36, this.y + this.height / 2, color);
139
+ if (this.rightIcon === 'more') this.drawMoreIcon(ctx, this.x + this.width - 36, this.y + this.height / 2, color);
140
+ }
141
+
244
142
  drawMenuIcon(ctx, x, y, color) {
245
143
  ctx.strokeStyle = color;
246
144
  ctx.lineWidth = 2;
@@ -253,10 +151,6 @@ class AppBar extends Component {
253
151
  }
254
152
  }
255
153
 
256
- /**
257
- * Dessine l'icône retour
258
- * @private
259
- */
260
154
  drawBackIcon(ctx, x, y, color) {
261
155
  ctx.strokeStyle = color;
262
156
  ctx.lineWidth = 2;
@@ -269,10 +163,6 @@ class AppBar extends Component {
269
163
  ctx.stroke();
270
164
  }
271
165
 
272
- /**
273
- * Dessine l'icône recherche
274
- * @private
275
- */
276
166
  drawSearchIcon(ctx, x, y, color) {
277
167
  ctx.strokeStyle = color;
278
168
  ctx.lineWidth = 2;
@@ -286,10 +176,6 @@ class AppBar extends Component {
286
176
  ctx.stroke();
287
177
  }
288
178
 
289
- /**
290
- * Dessine l'icône plus (3 dots)
291
- * @private
292
- */
293
179
  drawMoreIcon(ctx, x, y, color) {
294
180
  ctx.fillStyle = color;
295
181
  const spacing = this.platform === 'material' ? 10 : 8;
@@ -300,99 +186,71 @@ class AppBar extends Component {
300
186
  }
301
187
  }
302
188
 
303
- /**
304
- * Vérifie si un point est dans les zones cliquables
305
- */
306
- isPointInside(x, y) {
307
- // Les coordonnées x, y sont absolues, on les compare avec nos coordonnées absolues
189
+ handlePress(x, y) {
308
190
  const inY = y >= this.y && y <= this.y + this.height;
309
-
191
+
310
192
  if (!inY) return false;
311
-
312
- if (this.leftIcon && x >= this.x && x <= this.x + 56) return true;
313
- if (this.rightIcon && x >= this.x + this.width - 56 && x <= this.x + this.width) return true;
314
-
193
+
194
+ // Bouton gauche
195
+ if (this.leftIcon && x >= this.x && x <= this.x + 56) {
196
+ if (this.platform === 'material') this.addRipple(this.x + 28, this.y + this.height / 2);
197
+ else this.leftPressed = true, setTimeout(() => this.leftPressed = false, 150);
198
+ if (this.onLeftClick) this.onLeftClick();
199
+ return true;
200
+ }
201
+
202
+ // Bouton droit
203
+ if (this.rightIcon && x >= this.x + this.width - 56 && x <= this.x + this.width) {
204
+ if (this.platform === 'material') this.addRipple(this.x + this.width - 28, this.y + this.height / 2);
205
+ else this.rightPressed = true, setTimeout(() => this.rightPressed = false, 150);
206
+ if (this.onRightClick) this.onRightClick();
207
+ return true;
208
+ }
209
+
315
210
  return false;
316
211
  }
317
212
 
318
- /**
319
- * Gère la pression (clic)
320
- * @private
321
- */
322
- handlePress(x, y) {
323
- const adjustedY = y;
324
-
325
- if (adjustedY >= this.y && adjustedY <= this.y + this.height) {
326
- // Bouton gauche
327
- if (this.leftIcon && x >= this.x && x <= this.x + 56) {
328
- // Ripple effect (Material)
329
- if (this.platform === 'material') {
330
- this.ripples.push({
331
- x: this.x + 28,
332
- y: this.y + this.height / 2,
333
- radius: 0,
334
- maxRadius: 28,
335
- opacity: 1,
336
- createdAt: performance.now()
337
- });
338
-
339
- // Démarrer l'animation si elle n'est pas en cours
340
- if (!this.animationFrame) {
341
- this.startRippleAnimation();
342
- }
343
-
344
- // Forcer un redessin
345
- this.requestRender();
346
- } else {
347
- // iOS pressed state
348
- this.leftPressed = true;
349
- setTimeout(() => {
350
- this.leftPressed = false;
351
- this.requestRender();
352
- }, 150);
353
- }
354
-
355
- if (this.onLeftClick) this.onLeftClick();
356
- this.requestRender();
357
- return true;
358
- }
359
-
360
- // Bouton droit
361
- if (this.rightIcon && x >= this.x + this.width - 56 && x <= this.x + this.width) {
362
- // Ripple effect (Material)
363
- if (this.platform === 'material') {
364
- this.ripples.push({
365
- x: this.x + this.width - 28,
366
- y: this.y + this.height / 2,
367
- radius: 0,
368
- maxRadius: 28,
369
- opacity: 1,
370
- createdAt: performance.now()
371
- });
372
-
373
- // Démarrer l'animation si elle n'est pas en cours
374
- if (!this.animationFrame) {
375
- this.startRippleAnimation();
376
- }
377
-
378
- // Forcer un redessin
379
- this.requestRender();
380
- } else {
381
- // iOS pressed state
382
- this.rightPressed = true;
383
- setTimeout(() => {
384
- this.rightPressed = false;
385
- this.requestRender();
386
- }, 150);
387
- }
388
-
389
- if (this.onRightClick) this.onRightClick();
390
- this.requestRender();
391
- return true;
213
+ addRipple(x, y) {
214
+ this.ripples.push({
215
+ x, y,
216
+ radius: 0,
217
+ maxRadius: 28,
218
+ opacity: 1,
219
+ createdAt: performance.now()
220
+ });
221
+ if (!this.animationFrame) this.startRippleAnimation();
222
+ }
223
+
224
+ startRippleAnimation() {
225
+ const animate = (timestamp) => {
226
+ if (!this.lastAnimationTime) this.lastAnimationTime = timestamp;
227
+ const deltaTime = timestamp - this.lastAnimationTime;
228
+ this.lastAnimationTime = timestamp;
229
+ let needsUpdate = false;
230
+
231
+ for (let i = this.ripples.length - 1; i >= 0; i--) {
232
+ const ripple = this.ripples[i];
233
+ if (ripple.radius < ripple.maxRadius) ripple.radius += (ripple.maxRadius / 250) * deltaTime, needsUpdate = true;
234
+ if (ripple.radius >= ripple.maxRadius * 0.4) ripple.opacity -= (0.003 * deltaTime), ripple.opacity < 0 && (ripple.opacity = 0), needsUpdate = true;
235
+ if (ripple.opacity <= 0 && ripple.radius >= ripple.maxRadius * 0.95) this.ripples.splice(i, 1), needsUpdate = true;
392
236
  }
393
- }
394
- return false;
237
+
238
+ if (needsUpdate) this.requestRender();
239
+ if (this.ripples.length > 0) this.animationFrame = requestAnimationFrame(animate);
240
+ else this.animationFrame = null, this.lastAnimationTime = 0;
241
+ };
242
+
243
+ if (this.ripples.length > 0 && !this.animationFrame) this.animationFrame = requestAnimationFrame(animate);
244
+ }
245
+
246
+ requestRender() {
247
+ if (this.framework && this.framework.requestRender) this.framework.requestRender();
248
+ }
249
+
250
+ destroy() {
251
+ if (this.animationFrame) cancelAnimationFrame(this.animationFrame), this.animationFrame = null;
252
+ super.destroy();
395
253
  }
396
254
  }
397
255
 
398
- export default AppBar;
256
+ export default AppBar;