canvasframework 0.3.10 → 0.3.12

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.
@@ -50,12 +50,92 @@ class AppBar extends Component {
50
50
 
51
51
  // Ripple effect (Material uniquement)
52
52
  this.ripples = [];
53
+ this.animationFrame = null;
54
+ this.lastAnimationTime = 0;
55
+
56
+ // États pressed pour iOS
53
57
  this.leftPressed = false;
54
58
  this.rightPressed = false;
55
59
 
56
60
  this.onPress = this.handlePress.bind(this);
57
61
  }
58
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
+
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();
137
+ }
138
+
59
139
  /**
60
140
  * Dessine l'AppBar
61
141
  * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
@@ -98,13 +178,13 @@ class AppBar extends Component {
98
178
  if (this.leftPressed && this.leftIcon) {
99
179
  ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
100
180
  ctx.beginPath();
101
- ctx.arc(28, this.y + this.height / 2, 20, 0, Math.PI * 2);
181
+ ctx.arc(this.x + 28, this.y + this.height / 2, 20, 0, Math.PI * 2);
102
182
  ctx.fill();
103
183
  }
104
184
  if (this.rightPressed && this.rightIcon) {
105
185
  ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
106
186
  ctx.beginPath();
107
- ctx.arc(this.width - 28, this.y + this.height / 2, 20, 0, Math.PI * 2);
187
+ ctx.arc(this.x + this.width - 28, this.y + this.height / 2, 20, 0, Math.PI * 2);
108
188
  ctx.fill();
109
189
  }
110
190
  }
@@ -112,7 +192,7 @@ class AppBar extends Component {
112
192
  // Titre
113
193
  ctx.fillStyle = this.textColor;
114
194
  const titleAlign = this.platform === 'material' && this.leftIcon ? 'left' : 'center';
115
- const titleX = titleAlign === 'left' ? 72 : this.width / 2;
195
+ const titleX = titleAlign === 'left' ? this.x + 72 : this.x + this.width / 2;
116
196
  ctx.font = `${this.platform === 'material' ? 'bold ' : ''}20px -apple-system, Roboto, sans-serif`;
117
197
  ctx.textAlign = titleAlign;
118
198
  ctx.textBaseline = 'middle';
@@ -122,9 +202,9 @@ class AppBar extends Component {
122
202
  if (this.leftIcon) {
123
203
  const iconColor = this.platform === 'cupertino' ? '#007AFF' : this.textColor;
124
204
  if (this.leftIcon === 'menu') {
125
- this.drawMenuIcon(ctx, 16, this.y + this.height / 2, iconColor);
205
+ this.drawMenuIcon(ctx, this.x + 16, this.y + this.height / 2, iconColor);
126
206
  } else if (this.leftIcon === 'back') {
127
- this.drawBackIcon(ctx, 16, this.y + this.height / 2, iconColor);
207
+ this.drawBackIcon(ctx, this.x + 16, this.y + this.height / 2, iconColor);
128
208
  }
129
209
  }
130
210
 
@@ -132,9 +212,9 @@ class AppBar extends Component {
132
212
  if (this.rightIcon) {
133
213
  const iconColor = this.platform === 'cupertino' ? '#007AFF' : this.textColor;
134
214
  if (this.rightIcon === 'search') {
135
- this.drawSearchIcon(ctx, this.width - 36, this.y + this.height / 2, iconColor);
215
+ this.drawSearchIcon(ctx, this.x + this.width - 36, this.y + this.height / 2, iconColor);
136
216
  } else if (this.rightIcon === 'more') {
137
- this.drawMoreIcon(ctx, this.width - 36, this.y + this.height / 2, iconColor);
217
+ this.drawMoreIcon(ctx, this.x + this.width - 36, this.y + this.height / 2, iconColor);
138
218
  }
139
219
  }
140
220
 
@@ -157,35 +237,6 @@ class AppBar extends Component {
157
237
  }
158
238
  }
159
239
 
160
- /**
161
- * Anime les effets ripple
162
- * @private
163
- */
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
240
  /**
190
241
  * Dessine l'icône menu (hamburger)
191
242
  * @private
@@ -253,10 +304,14 @@ class AppBar extends Component {
253
304
  * Vérifie si un point est dans les zones cliquables
254
305
  */
255
306
  isPointInside(x, y) {
256
- if (y >= this.y && y <= this.y + this.height) {
257
- if (this.leftIcon && x >= 0 && x <= 56) return true;
258
- if (this.rightIcon && x >= this.width - 56 && x <= this.width) return true;
259
- }
307
+ // Les coordonnées x, y sont absolues, on les compare avec nos coordonnées absolues
308
+ const inY = y >= this.y && y <= this.y + this.height;
309
+
310
+ 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
+
260
315
  return false;
261
316
  }
262
317
 
@@ -269,46 +324,70 @@ class AppBar extends Component {
269
324
 
270
325
  if (adjustedY >= this.y && adjustedY <= this.y + this.height) {
271
326
  // Bouton gauche
272
- if (this.leftIcon && x >= 0 && x <= 56) {
327
+ if (this.leftIcon && x >= this.x && x <= this.x + 56) {
273
328
  // Ripple effect (Material)
274
329
  if (this.platform === 'material') {
275
330
  this.ripples.push({
276
- x: 28,
331
+ x: this.x + 28,
277
332
  y: this.y + this.height / 2,
278
333
  radius: 0,
279
334
  maxRadius: 28,
280
- opacity: 1
335
+ opacity: 1,
336
+ createdAt: performance.now()
281
337
  });
282
- this.animateRipple();
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();
283
346
  } else {
284
347
  // iOS pressed state
285
348
  this.leftPressed = true;
286
- setTimeout(() => { this.leftPressed = false; }, 150);
349
+ setTimeout(() => {
350
+ this.leftPressed = false;
351
+ this.requestRender();
352
+ }, 150);
287
353
  }
288
354
 
289
355
  if (this.onLeftClick) this.onLeftClick();
356
+ this.requestRender();
290
357
  return true;
291
358
  }
292
359
 
293
360
  // Bouton droit
294
- if (this.rightIcon && x >= this.width - 56 && x <= this.width) {
361
+ if (this.rightIcon && x >= this.x + this.width - 56 && x <= this.x + this.width) {
295
362
  // Ripple effect (Material)
296
363
  if (this.platform === 'material') {
297
364
  this.ripples.push({
298
- x: this.width - 28,
365
+ x: this.x + this.width - 28,
299
366
  y: this.y + this.height / 2,
300
367
  radius: 0,
301
368
  maxRadius: 28,
302
- opacity: 1
369
+ opacity: 1,
370
+ createdAt: performance.now()
303
371
  });
304
- this.animateRipple();
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();
305
380
  } else {
306
381
  // iOS pressed state
307
382
  this.rightPressed = true;
308
- setTimeout(() => { this.rightPressed = false; }, 150);
383
+ setTimeout(() => {
384
+ this.rightPressed = false;
385
+ this.requestRender();
386
+ }, 150);
309
387
  }
310
388
 
311
389
  if (this.onRightClick) this.onRightClick();
390
+ this.requestRender();
312
391
  return true;
313
392
  }
314
393
  }
@@ -49,6 +49,8 @@ class BottomNavigationBar extends Component {
49
49
 
50
50
  // Ripple effect (Material)
51
51
  this.ripples = [];
52
+ this.animationFrame = null;
53
+ this.lastAnimationTime = 0;
52
54
 
53
55
  // Animation de l'indicateur (iOS)
54
56
  this.indicatorX = 0;
@@ -61,6 +63,82 @@ class BottomNavigationBar extends Component {
61
63
  this.updateIndicatorPosition();
62
64
  }
63
65
 
66
+ /**
67
+ * Démarrer l'animation des ripples
68
+ * @private
69
+ */
70
+ startRippleAnimation() {
71
+ const animate = (timestamp) => {
72
+ if (!this.lastAnimationTime) this.lastAnimationTime = timestamp;
73
+ const deltaTime = timestamp - this.lastAnimationTime;
74
+ this.lastAnimationTime = timestamp;
75
+
76
+ let needsUpdate = false;
77
+
78
+ // Mettre à jour chaque ripple
79
+ for (let i = this.ripples.length - 1; i >= 0; i--) {
80
+ const ripple = this.ripples[i];
81
+
82
+ // Animer le rayon (expansion)
83
+ if (ripple.radius < ripple.maxRadius) {
84
+ ripple.radius += (ripple.maxRadius / 300) * deltaTime;
85
+ needsUpdate = true;
86
+ }
87
+
88
+ // Animer l'opacité (fade out)
89
+ if (ripple.radius >= ripple.maxRadius * 0.4) {
90
+ ripple.opacity -= (0.003 * deltaTime);
91
+ if (ripple.opacity < 0) ripple.opacity = 0;
92
+ needsUpdate = true;
93
+ }
94
+
95
+ // Supprimer les ripples terminés
96
+ if (ripple.opacity <= 0 && ripple.radius >= ripple.maxRadius * 0.95) {
97
+ this.ripples.splice(i, 1);
98
+ needsUpdate = true;
99
+ }
100
+ }
101
+
102
+ // Redessiner si nécessaire
103
+ if (needsUpdate) {
104
+ this.requestRender();
105
+ }
106
+
107
+ // Continuer l'animation
108
+ if (this.ripples.length > 0) {
109
+ this.animationFrame = requestAnimationFrame(animate);
110
+ } else {
111
+ this.animationFrame = null;
112
+ this.lastAnimationTime = 0;
113
+ }
114
+ };
115
+
116
+ if (this.ripples.length > 0 && !this.animationFrame) {
117
+ this.animationFrame = requestAnimationFrame(animate);
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Demander un redessin
123
+ * @private
124
+ */
125
+ requestRender() {
126
+ if (this.framework && this.framework.requestRender) {
127
+ this.framework.requestRender();
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Nettoyer l'animation lors de la destruction
133
+ */
134
+ destroy() {
135
+ if (this.animationFrame) {
136
+ cancelAnimationFrame(this.animationFrame);
137
+ this.animationFrame = null;
138
+ }
139
+ super.destroy();
140
+ }
141
+
64
142
  /**
65
143
  * Met à jour la position de l'indicateur iOS
66
144
  * @private
@@ -80,17 +158,30 @@ class BottomNavigationBar extends Component {
80
158
  */
81
159
  animateIndicator() {
82
160
  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;
161
+ const startTime = performance.now();
162
+ const duration = 300; // 300ms d'animation
163
+ const startX = this.indicatorX;
164
+ const endX = this.targetIndicatorX;
165
+
166
+ const animate = (currentTime) => {
167
+ const elapsed = currentTime - startTime;
168
+ const progress = Math.min(elapsed / duration, 1);
169
+
170
+ // Easing function (easeOutCubic)
171
+ const easeProgress = 1 - Math.pow(1 - progress, 3);
172
+ this.indicatorX = startX + (endX - startX) * easeProgress;
173
+
174
+ if (progress < 1) {
87
175
  requestAnimationFrame(animate);
176
+ this.requestRender();
88
177
  } else {
89
- this.indicatorX = this.targetIndicatorX;
178
+ this.indicatorX = endX;
90
179
  this.animatingIndicator = false;
180
+ this.requestRender();
91
181
  }
92
182
  };
93
- animate();
183
+
184
+ requestAnimationFrame(animate);
94
185
  }
95
186
 
96
187
  /**
@@ -120,11 +211,6 @@ class BottomNavigationBar extends Component {
120
211
  ctx.stroke();
121
212
  }
122
213
 
123
- // Ripples (Material)
124
- if (this.platform === 'material') {
125
- this.drawRipples(ctx);
126
- }
127
-
128
214
  // Items
129
215
  const itemWidth = this.width / this.items.length;
130
216
 
@@ -161,6 +247,11 @@ class BottomNavigationBar extends Component {
161
247
  ctx.fillText(item.label, itemX + itemWidth / 2, labelY);
162
248
  }
163
249
 
250
+ // Ripples (Material) - DESSINER APRÈS LES ÉLÉMENTS
251
+ if (this.platform === 'material') {
252
+ this.drawRipples(ctx);
253
+ }
254
+
164
255
  ctx.restore();
165
256
  }
166
257
 
@@ -169,44 +260,24 @@ class BottomNavigationBar extends Component {
169
260
  * @private
170
261
  */
171
262
  drawRipples(ctx) {
263
+ // Sauvegarder le contexte
264
+ ctx.save();
265
+
266
+ // Créer un masque de clipping pour limiter les ripples à la barre
267
+ ctx.beginPath();
268
+ ctx.rect(this.x, this.y, this.width, this.height);
269
+ ctx.clip();
270
+
172
271
  for (let ripple of this.ripples) {
173
- ctx.save();
174
272
  ctx.globalAlpha = ripple.opacity;
175
273
  ctx.fillStyle = this.rippleColor;
176
274
  ctx.beginPath();
177
275
  ctx.arc(ripple.x, ripple.y, ripple.radius, 0, Math.PI * 2);
178
276
  ctx.fill();
179
- ctx.restore();
180
277
  }
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
278
 
209
- animate();
279
+ // Restaurer le contexte
280
+ ctx.restore();
210
281
  }
211
282
 
212
283
  /**
@@ -308,32 +379,52 @@ class BottomNavigationBar extends Component {
308
379
  * @private
309
380
  */
310
381
  handlePress(x, y) {
311
- const itemWidth = this.width / this.items.length;
312
- const index = Math.floor(x / itemWidth);
382
+ // Convertir les coordonnées absolues en coordonnées relatives à la barre
383
+ const relativeX = x - this.x;
384
+ const relativeY = y - this.y;
313
385
 
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
-
327
- this.selectedIndex = index;
328
- this.updateIndicatorPosition();
386
+ // Vérifier si on est dans la barre
387
+ if (relativeY >= 0 && relativeY <= this.height) {
388
+ const itemWidth = this.width / this.items.length;
389
+ const index = Math.floor(relativeX / itemWidth);
329
390
 
330
- // Animer l'indicateur (iOS)
331
- if (this.platform === 'cupertino') {
332
- this.animateIndicator();
333
- }
334
-
335
- if (this.onChange) {
336
- this.onChange(index, this.items[index]);
391
+ if (index >= 0 && index < this.items.length && index !== this.selectedIndex) {
392
+ // Ripple effect (Material)
393
+ if (this.platform === 'material') {
394
+ // Calculer la taille maximale du ripple (ne pas dépasser la hauteur de la barre)
395
+ const maxRippleRadius = Math.min(itemWidth * 0.6, this.height * 0.8);
396
+
397
+ this.ripples.push({
398
+ x: this.x + (index + 0.5) * itemWidth, // Coordonnée absolue
399
+ y: this.y + this.height / 2, // Coordonnée absolue
400
+ radius: 0,
401
+ maxRadius: maxRippleRadius,
402
+ opacity: 1,
403
+ createdAt: performance.now()
404
+ });
405
+
406
+ // Démarrer l'animation si elle n'est pas en cours
407
+ if (!this.animationFrame) {
408
+ this.startRippleAnimation();
409
+ }
410
+
411
+ // Forcer un redessin
412
+ this.requestRender();
413
+ }
414
+
415
+ this.selectedIndex = index;
416
+ this.updateIndicatorPosition();
417
+
418
+ // Animer l'indicateur (iOS)
419
+ if (this.platform === 'cupertino') {
420
+ this.animateIndicator();
421
+ }
422
+
423
+ if (this.onChange) {
424
+ this.onChange(index, this.items[index]);
425
+ }
426
+
427
+ this.requestRender();
337
428
  }
338
429
  }
339
430
  }