canvasframework 0.3.15 → 0.3.16

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.
@@ -14,97 +14,140 @@ import Component from '../core/Component.js';
14
14
  class SegmentedControl extends Component {
15
15
  constructor(framework, options = {}) {
16
16
  super(framework, options);
17
- /**
18
- * Plateforme : "material" ou "cupertino"
19
- * @type {string}
20
- */
17
+
21
18
  this.platform = framework.platform;
22
-
23
- /**
24
- * Liste des boutons
25
- * @type {Array<{text: string, onClick?: Function}>}
26
- */
27
19
  this.buttons = options.buttons || [{ text: 'One' }, { text: 'Two' }, { text: 'Three' }];
28
-
29
- /**
30
- * Index du segment sélectionné
31
- * @type {number}
32
- */
33
20
  this.selectedIndex = options.selectedIndex || 0;
34
-
35
- /**
36
- * Hauteur du contrôle
37
- * @type {number}
38
- */
39
21
  this.height = options.height || 40;
40
-
41
- /**
42
- * Espacement entre segments
43
- * @type {number}
44
- */
45
22
  this.spacing = options.spacing || 1;
46
-
47
- /**
48
- * Ripples Material
49
- * @type {Array<{x: number, y: number, index: number, radius: number, maxRadius: number, opacity: number}>}
50
- */
23
+
24
+ // IMPORTANT: Lier les handlers d'événements
25
+ this.onPress = this.handlePress.bind(this);
26
+ this.onRelease = this.handleRelease.bind(this);
27
+
28
+ // État pour les animations
51
29
  this.ripples = [];
52
-
53
- /**
54
- * Index temporaire pressé pour Cupertino
55
- * @type {number|null}
56
- */
57
30
  this.pressedIndex = null;
31
+ this._isAnimating = false;
32
+
58
33
  }
59
34
 
60
35
  /**
61
36
  * Gère la pression sur un segment
62
37
  * @param {number} x - Coordonnée X du clic
63
38
  * @param {number} y - Coordonnée Y du clic
39
+ * @returns {boolean} True si un segment a été cliqué
64
40
  */
65
41
  handlePress(x, y) {
66
42
  const index = this.getButtonIndexAt(x, y);
43
+
67
44
  if (index !== null) {
45
+ // Sauvegarder l'index pressé pour l'animation
46
+ this.pressedIndex = index;
47
+
48
+ // Pour Material: créer un ripple
68
49
  if (this.platform === 'material') {
69
50
  const btnWidth = (this.width - this.spacing * (this.buttons.length - 1)) / this.buttons.length;
70
51
  const btnX = this.x + index * (btnWidth + this.spacing);
52
+
71
53
  this.ripples.push({
72
- x: x - btnX,
73
- y: y - this.y,
54
+ x: x - btnX, // Position relative au bouton
55
+ y: y - this.y, // Position relative au bouton
74
56
  index: index,
75
57
  radius: 0,
76
58
  maxRadius: Math.max(btnWidth, this.height) * 1.5,
77
- opacity: 1
59
+ opacity: 0.3,
60
+ startTime: Date.now()
78
61
  });
79
- this.animateRipple();
80
- } else {
81
- this.pressedIndex = index;
82
- setTimeout(() => this.pressedIndex = null, 150);
62
+
63
+ // Démarrer l'animation si pas déjà en cours
64
+ if (!this._isAnimating) {
65
+ this._animate();
66
+ }
83
67
  }
84
-
68
+
69
+ // Sélectionner le segment
85
70
  this.selectedIndex = index;
86
- if (this.buttons[index].onClick) this.buttons[index].onClick(index);
71
+
72
+ // Forcer le redessin immédiat
73
+ this._requestRedraw();
74
+
75
+ return true;
76
+ }
77
+
78
+ return false;
79
+ }
80
+
81
+ /**
82
+ * Gère le relâchement
83
+ * @param {number} x - Coordonnée X
84
+ * @param {number} y - Coordonnée Y
85
+ */
86
+ handleRelease(x, y) {
87
+ const index = this.getButtonIndexAt(x, y);
88
+
89
+ if (index !== null && index === this.pressedIndex) {
90
+ // Appeler le callback si défini
91
+ if (this.buttons[index].onClick) {
92
+ this.buttons[index].onClick(index);
93
+ }
87
94
  }
95
+
96
+ // Réinitialiser l'index pressé
97
+ this.pressedIndex = null;
98
+
99
+ // Forcer le redessin
100
+ this._requestRedraw();
88
101
  }
89
102
 
90
103
  /**
91
104
  * Anime les ripples Material
92
105
  * @private
93
106
  */
94
- animateRipple() {
95
- const animate = () => {
96
- let active = false;
97
- for (let ripple of this.ripples) {
98
- if (ripple.radius < ripple.maxRadius) {
99
- ripple.radius += ripple.maxRadius / 15;
100
- active = true;
107
+ _animate() {
108
+ this._isAnimating = true;
109
+
110
+ const animateFrame = () => {
111
+ let hasActiveRipples = false;
112
+ const now = Date.now();
113
+
114
+ // Mettre à jour tous les ripples
115
+ for (let i = this.ripples.length - 1; i >= 0; i--) {
116
+ const ripple = this.ripples[i];
117
+ const elapsed = now - ripple.startTime;
118
+
119
+ // Animation sur 600ms
120
+ const progress = Math.min(elapsed / 600, 1);
121
+
122
+ // Équation d'easing
123
+ const easedProgress = 1 - Math.pow(1 - progress, 3);
124
+
125
+ // Mettre à jour le rayon
126
+ ripple.radius = ripple.maxRadius * easedProgress;
127
+
128
+ // Diminuer l'opacité après 50% de progression
129
+ if (progress > 0.5) {
130
+ ripple.opacity = 0.3 * (1 - (progress - 0.5) * 2);
131
+ }
132
+
133
+ // Supprimer les ripples terminés
134
+ if (progress >= 1) {
135
+ this.ripples.splice(i, 1);
136
+ } else {
137
+ hasActiveRipples = true;
101
138
  }
102
- if (ripple.radius >= ripple.maxRadius * 0.5) ripple.opacity -= 0.05;
103
139
  }
104
- this.ripples = this.ripples.filter(r => r.opacity > 0);
105
- if (active) requestAnimationFrame(animate);
140
+
141
+ // Redessiner si il y a des ripples actifs
142
+ if (hasActiveRipples) {
143
+ this._requestRedraw();
144
+ requestAnimationFrame(animateFrame);
145
+ } else {
146
+ this._isAnimating = false;
147
+ }
106
148
  };
107
- animate();
149
+
150
+ requestAnimationFrame(animateFrame);
108
151
  }
109
152
 
110
153
  /**
@@ -115,36 +158,103 @@ class SegmentedControl extends Component {
115
158
  * @private
116
159
  */
117
160
  getButtonIndexAt(x, y) {
161
+ // Vérifier si dans les limites verticales
162
+ if (y < this.y || y > this.y + this.height) {
163
+ return null;
164
+ }
165
+
118
166
  const btnWidth = (this.width - this.spacing * (this.buttons.length - 1)) / this.buttons.length;
119
- if (y < this.y || y > this.y + this.height) return null;
167
+
120
168
  for (let i = 0; i < this.buttons.length; i++) {
121
169
  const btnX = this.x + i * (btnWidth + this.spacing);
122
- if (x >= btnX && x <= btnX + btnWidth) return i;
170
+
171
+ // Vérifier si dans les limites horizontales du bouton
172
+ if (x >= btnX && x <= btnX + btnWidth) {
173
+ return i;
174
+ }
123
175
  }
176
+
124
177
  return null;
125
178
  }
179
+
180
+ /**
181
+ * Force le redessin du composant
182
+ * @private
183
+ */
184
+ _requestRedraw() {
185
+ if (this.framework && this.framework.markComponentDirty) {
186
+ this.framework.markComponentDirty(this);
187
+ }
188
+ }
126
189
 
127
190
  /**
128
191
  * Dessine le SegmentedControl
129
192
  * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
130
193
  */
131
194
  draw(ctx) {
195
+ ctx.save();
196
+
132
197
  const btnWidth = (this.width - this.spacing * (this.buttons.length - 1)) / this.buttons.length;
133
-
134
- this.buttons.forEach((btn, i) => {
198
+ const radius = this.height / 2;
199
+
200
+ // Dessiner tous les boutons
201
+ for (let i = 0; i < this.buttons.length; i++) {
202
+ const btn = this.buttons[i];
135
203
  const btnX = this.x + i * (btnWidth + this.spacing);
136
-
137
- // Background
204
+
205
+ // Couleurs selon la plateforme et l'état
206
+ let backgroundColor;
207
+ let textColor;
208
+
138
209
  if (this.platform === 'material') {
139
- ctx.fillStyle = this.selectedIndex === i ? '#6200EE' : '#E0E0E0';
210
+ // Material Design
211
+ if (this.selectedIndex === i) {
212
+ backgroundColor = '#6200EE'; // Violet Material
213
+ textColor = '#FFFFFF';
214
+ } else {
215
+ backgroundColor = '#E0E0E0'; // Gris clair
216
+ textColor = '#000000';
217
+ }
218
+
219
+ // Si pressé (mais pas encore sélectionné)
220
+ if (this.pressedIndex === i && this.pressedIndex !== this.selectedIndex) {
221
+ backgroundColor = 'rgba(98, 0, 238, 0.12)'; // Violet très transparent
222
+ }
140
223
  } else {
141
- ctx.fillStyle = this.selectedIndex === i ? '#007AFF' : '#F0F0F0';
142
- if (this.pressedIndex === i) ctx.fillStyle = '#D9D9D9';
224
+ // iOS/Cupertino
225
+ if (this.selectedIndex === i) {
226
+ backgroundColor = '#007AFF'; // Bleu iOS
227
+ textColor = '#FFFFFF';
228
+ } else {
229
+ backgroundColor = '#F0F0F0'; // Gris très clair iOS
230
+ textColor = '#000000';
231
+ }
232
+
233
+ // Si pressé
234
+ if (this.pressedIndex === i) {
235
+ backgroundColor = this.selectedIndex === i ? '#0056CC' : '#D9D9D9';
236
+ }
143
237
  }
144
-
145
- const radius = this.height / 2;
238
+
239
+ // Dessiner le fond du bouton
240
+ ctx.fillStyle = backgroundColor;
241
+
242
+ // Coins arrondis selon la position
146
243
  ctx.beginPath();
147
- if (i === 0) {
244
+
245
+ if (i === 0 && i === this.buttons.length - 1) {
246
+ // Un seul bouton - tous les coins arrondis
247
+ ctx.moveTo(btnX + radius, this.y);
248
+ ctx.lineTo(btnX + btnWidth - radius, this.y);
249
+ ctx.quadraticCurveTo(btnX + btnWidth, this.y, btnX + btnWidth, this.y + radius);
250
+ ctx.lineTo(btnX + btnWidth, this.y + this.height - radius);
251
+ ctx.quadraticCurveTo(btnX + btnWidth, this.y + this.height, btnX + btnWidth - radius, this.y + this.height);
252
+ ctx.lineTo(btnX + radius, this.y + this.height);
253
+ ctx.quadraticCurveTo(btnX, this.y + this.height, btnX, this.y + this.height - radius);
254
+ ctx.lineTo(btnX, this.y + radius);
255
+ ctx.quadraticCurveTo(btnX, this.y, btnX + radius, this.y);
256
+ } else if (i === 0) {
257
+ // Premier bouton - coins gauche arrondis
148
258
  ctx.moveTo(btnX + radius, this.y);
149
259
  ctx.lineTo(btnX + btnWidth, this.y);
150
260
  ctx.lineTo(btnX + btnWidth, this.y + this.height);
@@ -153,6 +263,7 @@ class SegmentedControl extends Component {
153
263
  ctx.lineTo(btnX, this.y + radius);
154
264
  ctx.quadraticCurveTo(btnX, this.y, btnX + radius, this.y);
155
265
  } else if (i === this.buttons.length - 1) {
266
+ // Dernier bouton - coins droit arrondis
156
267
  ctx.moveTo(btnX, this.y);
157
268
  ctx.lineTo(btnX + btnWidth - radius, this.y);
158
269
  ctx.quadraticCurveTo(btnX + btnWidth, this.y, btnX + btnWidth, this.y + radius);
@@ -160,32 +271,76 @@ class SegmentedControl extends Component {
160
271
  ctx.quadraticCurveTo(btnX + btnWidth, this.y + this.height, btnX + btnWidth - radius, this.y + this.height);
161
272
  ctx.lineTo(btnX, this.y + this.height);
162
273
  } else {
274
+ // Bouton du milieu - coins carrés
163
275
  ctx.rect(btnX, this.y, btnWidth, this.height);
164
276
  }
277
+
278
+ ctx.closePath();
165
279
  ctx.fill();
166
-
167
- // Texte
168
- ctx.fillStyle = this.platform === 'material'
169
- ? (this.selectedIndex === i ? '#FFF' : '#000')
170
- : (this.selectedIndex === i ? '#FFF' : '#000');
171
- ctx.font = `${this.height / 2}px -apple-system, Roboto, sans-serif`;
280
+
281
+ // Dessiner les ripples Material
282
+ if (this.platform === 'material') {
283
+ for (const ripple of this.ripples) {
284
+ if (ripple.index === i) {
285
+ ctx.save();
286
+
287
+ // Clip sur le bouton pour que le ripple ne dépasse pas
288
+ ctx.beginPath();
289
+ if (i === 0) {
290
+ ctx.moveTo(btnX + radius, this.y);
291
+ ctx.lineTo(btnX + btnWidth, this.y);
292
+ ctx.lineTo(btnX + btnWidth, this.y + this.height);
293
+ ctx.lineTo(btnX + radius, this.y + this.height);
294
+ ctx.quadraticCurveTo(btnX, this.y + this.height, btnX, this.y + this.height - radius);
295
+ ctx.lineTo(btnX, this.y + radius);
296
+ ctx.quadraticCurveTo(btnX, this.y, btnX + radius, this.y);
297
+ } else if (i === this.buttons.length - 1) {
298
+ ctx.moveTo(btnX, this.y);
299
+ ctx.lineTo(btnX + btnWidth - radius, this.y);
300
+ ctx.quadraticCurveTo(btnX + btnWidth, this.y, btnX + btnWidth, this.y + radius);
301
+ ctx.lineTo(btnX + btnWidth, this.y + this.height - radius);
302
+ ctx.quadraticCurveTo(btnX + btnWidth, this.y + this.height, btnX + btnWidth - radius, this.y + this.height);
303
+ ctx.lineTo(btnX, this.y + this.height);
304
+ } else {
305
+ ctx.rect(btnX, this.y, btnWidth, this.height);
306
+ }
307
+ ctx.closePath();
308
+ ctx.clip();
309
+
310
+ // Dessiner le ripple
311
+ ctx.fillStyle = `rgba(255, 255, 255, ${ripple.opacity})`;
312
+ ctx.beginPath();
313
+ ctx.arc(btnX + ripple.x, this.y + ripple.y, ripple.radius, 0, Math.PI * 2);
314
+ ctx.fill();
315
+
316
+ ctx.restore();
317
+ }
318
+ }
319
+ }
320
+
321
+ // Dessiner le texte
322
+ ctx.fillStyle = textColor;
323
+ ctx.font = `500 ${this.height / 2.5}px -apple-system, Roboto, sans-serif`;
172
324
  ctx.textAlign = 'center';
173
325
  ctx.textBaseline = 'middle';
174
326
  ctx.fillText(btn.text || `Button ${i + 1}`, btnX + btnWidth / 2, this.y + this.height / 2);
175
- });
176
-
177
- // Ripples Material
178
- if (this.platform === 'material' && this.ripples.length) {
179
- ctx.save();
180
- this.ripples.forEach(r => {
181
- const btnX = this.x + r.index * (btnWidth + this.spacing);
327
+ }
328
+
329
+ // Dessiner les séparateurs (pour iOS)
330
+ if (this.platform === 'cupertino' && this.buttons.length > 1) {
331
+ ctx.strokeStyle = '#C7C7CC';
332
+ ctx.lineWidth = 1;
333
+
334
+ for (let i = 1; i < this.buttons.length; i++) {
335
+ const separatorX = this.x + i * btnWidth + (i - 1) * this.spacing;
182
336
  ctx.beginPath();
183
- ctx.arc(btnX + r.x, this.y + r.y, r.radius, 0, Math.PI * 2);
184
- ctx.fillStyle = `rgba(255,255,255,${r.opacity})`;
185
- ctx.fill();
186
- });
187
- ctx.restore();
337
+ ctx.moveTo(separatorX, this.y + 8);
338
+ ctx.lineTo(separatorX, this.y + this.height - 8);
339
+ ctx.stroke();
340
+ }
188
341
  }
342
+
343
+ ctx.restore();
189
344
  }
190
345
 
191
346
  /**
@@ -199,4 +354,4 @@ class SegmentedControl extends Component {
199
354
  }
200
355
  }
201
356
 
202
- export default SegmentedControl;
357
+ export default SegmentedControl;