canvasframework 0.6.3 → 0.7.1

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.
package/components/FAB.js CHANGED
@@ -1,269 +1,196 @@
1
1
  import Component from '../core/Component.js';
2
+ import { roundRect, hexToRgb, hexToRgba, darkenColor } from '../core/CanvasUtils.js';
2
3
 
3
4
  /**
4
- * Bouton d'action flottant (Material Design 3)
5
- * @class
6
- * @extends Component
7
- * @property {string} icon - Icône du bouton
8
- * @property {boolean} extended - Mode étendu (avec texte)
9
- * @property {string} text - Texte (en mode étendu)
10
- * @property {string} platform - Plateforme
11
- * @property {string} variant - Variante Material 3: 'small', 'medium', 'large', 'extended'
12
- * @property {number} size - Taille du bouton
13
- * @property {string} bgColor - Couleur de fond
14
- * @property {string} iconColor - Couleur de l'icône
15
- * @property {Array} ripples - Effets ripple
5
+ * FAB Floating Action Button (Material Design 3 + Cupertino).
6
+ *
7
+ * Variantes : 'small' | 'medium' | 'large' | 'extended'
8
+ *
9
+ * Corrections :
10
+ * - Utilitaires couleur/dessin viennent de CanvasUtils
11
+ * - markDirty() utilisé à la place de redraw() global
12
+ * - Guard _destroyed dans le RAF
13
+ * - destroy() annule le RAF
16
14
  */
17
15
  class FAB extends Component {
18
16
  /**
19
- * Crée une instance de FAB
20
- * @param {CanvasFramework} framework - Framework parent
21
- * @param {Object} [options={}] - Options de configuration
22
- * @param {string} [options.icon='+'] - Icône
23
- * @param {boolean} [options.extended=false] - Mode étendu
24
- * @param {string} [options.text=''] - Texte (mode étendu)
25
- * @param {string} [options.variant='medium'] - Variante: 'small', 'medium', 'large', 'extended'
26
- * @param {string} [options.bgColor] - Couleur (auto selon platform)
27
- * @param {string} [options.iconColor='#FFFFFF'] - Couleur de l'icône
17
+ * @param {CanvasFramework} framework
18
+ * @param {Object} [options={}]
19
+ * @param {string} [options.icon='+']
20
+ * @param {boolean} [options.extended=false]
21
+ * @param {string} [options.text='']
22
+ * @param {string} [options.variant='medium'] - 'small'|'medium'|'large'|'extended'
23
+ * @param {string} [options.bgColor]
24
+ * @param {string} [options.iconColor='#FFFFFF']
28
25
  */
29
26
  constructor(framework, options = {}) {
30
27
  super(framework, options);
31
-
32
- this.icon = options.icon || '+';
28
+
29
+ this.icon = options.icon || '+';
33
30
  this.extended = options.extended || false;
34
- this.text = options.text || '';
31
+ this.text = options.text || '';
35
32
  this.platform = framework.platform;
36
- this.variant = options.variant || 'medium';
37
-
38
- // Tailles selon Material Design 3
39
- const sizes = {
40
- small: 40,
41
- medium: 56,
42
- large: 96
43
- };
44
-
33
+ this.variant = options.variant || 'medium';
34
+
35
+ const sizes = { small: 40, medium: 56, large: 96 };
45
36
  this.size = options.size || sizes[this.variant] || 56;
46
-
47
- // Couleurs Material 3
48
- this.bgColor = options.bgColor || (framework.platform === 'material' ? '#6750A4' : '#007AFF');
37
+
38
+ this.bgColor = options.bgColor || (framework.platform === 'material' ? '#6750A4' : '#007AFF');
49
39
  this.iconColor = options.iconColor || '#FFFFFF';
50
-
51
- // Border radius selon Material 3 (pas circulaire!)
52
- this.borderRadius = {
53
- small: 12,
54
- medium: 16,
55
- large: 28,
56
- extended: 16
57
- }[this.variant] || 16;
58
-
59
- // Position par défaut en bas à droite
60
- this.x = options.x !== undefined ? options.x : framework.width - this.size - 16;
40
+
41
+ this.borderRadius = { small: 12, medium: 16, large: 28, extended: 16 }[this.variant] || 16;
42
+
43
+ // Position par défaut : coin bas-droit
44
+ this.x = options.x !== undefined ? options.x : framework.width - this.size - 16;
61
45
  this.y = options.y !== undefined ? options.y : framework.height - this.size - 80;
62
-
63
- // Si extended, ajuster la largeur
46
+
64
47
  if (this.extended && this.text) {
65
48
  const ctx = framework.ctx;
66
49
  ctx.save();
67
- ctx.font = 'bold 14px -apple-system, sans-serif';
50
+ ctx.font = 'bold 14px -apple-system, sans-serif';
68
51
  const textWidth = ctx.measureText(this.text).width;
69
52
  ctx.restore();
70
- this.width = this.size + textWidth + 24;
53
+ this.width = this.size + textWidth + 24;
71
54
  this.borderRadius = 16;
72
55
  } else {
73
56
  this.width = this.size;
74
57
  }
75
58
  this.height = this.size;
76
-
77
- // Effet ripple
59
+
78
60
  this.ripples = [];
79
-
80
- // ✅ CORRECTION : Binder onPress comme dans Button
81
- this.onPress = this.handlePress.bind(this);
61
+ this._rafId = null;
62
+
63
+ this.onPress = this._handlePress.bind(this);
82
64
  }
83
-
84
- /**
85
- * Gère la pression sur le FAB
86
- * @param {number} x - Coordonnée X
87
- * @param {number} y - Coordonnée Y
88
- * @private
89
- */
90
- handlePress(x, y) {
91
- // Créer un ripple au point de clic (Material uniquement)
65
+
66
+ // ─────────────────────────────────────────
67
+ // INTERACTIONS
68
+ // ─────────────────────────────────────────
69
+
70
+ /** @private */
71
+ _handlePress(x, y) {
92
72
  if (this.platform === 'material') {
93
- const adjustedY = y - this.framework.scrollOffset;
73
+ const adjY = y - (this.framework.scrollOffset || 0);
94
74
  this.ripples.push({
95
75
  x: x - this.x,
96
- y: adjustedY - this.y,
76
+ y: adjY - this.y,
97
77
  radius: 0,
98
78
  maxRadius: Math.max(this.width, this.height) * 1.5,
99
- opacity: 1
79
+ opacity: 1,
100
80
  });
101
- this.animateRipple();
81
+ this._animateRipple();
102
82
  }
103
83
  }
104
-
105
- /**
106
- * Anime l'effet ripple
107
- * @private
108
- */
109
- animateRipple() {
84
+
85
+ /** @private */
86
+ _animateRipple() {
87
+ if (this._rafId) return;
88
+
110
89
  const animate = () => {
111
- let hasActiveRipples = false;
112
-
113
- for (let ripple of this.ripples) {
114
- if (ripple.radius < ripple.maxRadius) {
115
- ripple.radius += ripple.maxRadius / 15;
116
- hasActiveRipples = true;
117
- }
118
-
119
- // Fade out après 50% de l'expansion
120
- if (ripple.radius >= ripple.maxRadius * 0.5) {
121
- ripple.opacity -= 0.05;
122
- }
90
+ if (this._destroyed) { this._rafId = null; return; }
91
+
92
+ let hasActive = false;
93
+ for (const r of this.ripples) {
94
+ if (r.radius < r.maxRadius) { r.radius += r.maxRadius / 15; hasActive = true; }
95
+ if (r.radius >= r.maxRadius * 0.5) r.opacity -= 0.05;
123
96
  }
124
-
125
- // Nettoyer les ripples terminés
126
- this.ripples = this.ripples.filter(r => r.opacity > 0);
127
-
128
- if (hasActiveRipples) {
129
- requestAnimationFrame(animate);
97
+ this.ripples = this.ripples.filter((r) => r.opacity > 0);
98
+
99
+ this.markDirty();
100
+
101
+ if (hasActive && this.ripples.length > 0) {
102
+ this._rafId = requestAnimationFrame(animate);
103
+ } else {
104
+ this._rafId = null;
130
105
  }
131
106
  };
132
-
133
- animate();
107
+
108
+ this._rafId = requestAnimationFrame(animate);
134
109
  }
135
-
136
- /**
137
- * Dessine le FAB
138
- * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
139
- */
110
+
111
+ // ─────────────────────────────────────────
112
+ // DESSIN
113
+ // ─────────────────────────────────────────
114
+
140
115
  draw(ctx) {
141
116
  ctx.save();
142
-
143
- // Ombre (elevation)
117
+
144
118
  if (!this.pressed) {
145
- ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
146
- ctx.shadowBlur = this.platform === 'material' ? 8 : 12;
119
+ ctx.shadowColor = 'rgba(0,0,0,0.3)';
120
+ ctx.shadowBlur = this.platform === 'material' ? 8 : 12;
147
121
  ctx.shadowOffsetY = this.platform === 'material' ? 4 : 6;
148
122
  }
149
-
150
- // Background - Material 3: rectangles arrondis, pas cercles!
151
- ctx.fillStyle = this.pressed ? this.darkenColor(this.bgColor) : this.bgColor;
123
+
124
+ ctx.fillStyle = this.pressed ? darkenColor(this.bgColor) : this.bgColor;
152
125
  ctx.beginPath();
153
- this.roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
126
+ roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
154
127
  ctx.fill();
155
-
156
- ctx.shadowColor = 'transparent';
157
- ctx.shadowBlur = 0;
128
+
129
+ ctx.shadowColor = 'transparent';
130
+ ctx.shadowBlur = 0;
158
131
  ctx.shadowOffsetY = 0;
159
-
160
- // Clipping pour les ripples (Material uniquement)
161
- if (this.platform === 'material') {
132
+
133
+ // Ripple (Material)
134
+ if (this.platform === 'material' && this.ripples.length > 0) {
162
135
  ctx.save();
163
136
  ctx.beginPath();
164
- this.roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
137
+ roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
165
138
  ctx.clip();
166
-
167
- // Dessiner les ripples
168
- for (let ripple of this.ripples) {
169
- ctx.globalAlpha = ripple.opacity;
170
- ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
139
+
140
+ for (const r of this.ripples) {
141
+ ctx.globalAlpha = r.opacity;
142
+ ctx.fillStyle = 'rgba(255,255,255,0.3)';
171
143
  ctx.beginPath();
172
- ctx.arc(this.x + ripple.x, this.y + ripple.y, ripple.radius, 0, Math.PI * 2);
144
+ ctx.arc(this.x + r.x, this.y + r.y, r.radius, 0, Math.PI * 2);
173
145
  ctx.fill();
174
146
  }
175
-
176
147
  ctx.restore();
177
148
  }
178
-
179
- // Overlay si pressed (iOS)
149
+
150
+ // Overlay pressé (iOS)
180
151
  if (this.pressed && this.platform === 'cupertino') {
181
- ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
152
+ ctx.fillStyle = 'rgba(0,0,0,0.1)';
182
153
  ctx.beginPath();
183
- this.roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
154
+ roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
184
155
  ctx.fill();
185
156
  }
186
-
157
+
187
158
  // Icône
188
- ctx.fillStyle = this.iconColor;
189
- const iconSize = this.variant === 'large' ? 36 : 24;
190
- ctx.font = `bold ${iconSize}px sans-serif`;
191
- ctx.textAlign = 'center';
159
+ ctx.fillStyle = this.iconColor;
160
+ const iconSize = this.variant === 'large' ? 36 : 24;
161
+ ctx.font = `bold ${iconSize}px sans-serif`;
162
+ ctx.textAlign = 'center';
192
163
  ctx.textBaseline = 'middle';
193
-
164
+
194
165
  if (this.extended && this.text) {
195
- // Icône à gauche
196
166
  ctx.fillText(this.icon, this.x + this.size / 2, this.y + this.size / 2);
197
-
198
- // Texte à droite
199
167
  ctx.font = 'bold 14px -apple-system, sans-serif';
200
168
  ctx.fillText(this.text, this.x + this.size + 12, this.y + this.size / 2);
201
169
  } else {
202
- // Icône centrée
203
170
  ctx.fillText(this.icon, this.x + this.width / 2, this.y + this.height / 2);
204
171
  }
205
-
172
+
206
173
  ctx.restore();
207
174
  }
208
-
209
- /**
210
- * Dessine un rectangle avec coins arrondis
211
- * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
212
- * @param {number} x - Position X
213
- * @param {number} y - Position Y
214
- * @param {number} width - Largeur
215
- * @param {number} height - Hauteur
216
- * @param {number} radius - Rayon des coins
217
- * @private
218
- */
219
- roundRect(ctx, x, y, width, height, radius) {
220
- ctx.moveTo(x + radius, y);
221
- ctx.lineTo(x + width - radius, y);
222
- ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
223
- ctx.lineTo(x + width, y + height - radius);
224
- ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
225
- ctx.lineTo(x + radius, y + height);
226
- ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
227
- ctx.lineTo(x, y + radius);
228
- ctx.quadraticCurveTo(x, y, x + radius, y);
229
- }
230
-
231
- /**
232
- * Assombrit une couleur
233
- * @param {string} color - Couleur hexadécimale
234
- * @returns {string} Couleur assombrie
235
- * @private
236
- */
237
- darkenColor(color) {
238
- const rgb = this.hexToRgb(color);
239
- return `rgb(${Math.max(0, rgb.r - 30)}, ${Math.max(0, rgb.g - 30)}, ${Math.max(0, rgb.b - 30)})`;
240
- }
241
-
242
- /**
243
- * Convertit une couleur hex en RGB
244
- * @param {string} hex - Couleur hexadécimale
245
- * @returns {{r: number, g: number, b: number}} Objet RGB
246
- * @private
247
- */
248
- hexToRgb(hex) {
249
- const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
250
- return result ? {
251
- r: parseInt(result[1], 16),
252
- g: parseInt(result[2], 16),
253
- b: parseInt(result[3], 16)
254
- } : { r: 0, g: 0, b: 0 };
255
- }
256
-
257
- /**
258
- * Vérifie si un point est dans les limites
259
- * @param {number} x - Coordonnée X
260
- * @param {number} y - Coordonnée Y
261
- * @returns {boolean} True si le point est dans le FAB
262
- */
175
+
263
176
  isPointInside(x, y) {
264
- // Material 3: toujours des rectangles arrondis, plus de cercles
265
- return x >= this.x && x <= this.x + this.width &&
266
- y >= this.y && y <= this.y + this.height;
177
+ return (
178
+ x >= this.x && x <= this.x + this.width &&
179
+ y >= this.y && y <= this.y + this.height
180
+ );
181
+ }
182
+
183
+ // ─────────────────────────────────────────
184
+ // DESTROY
185
+ // ─────────────────────────────────────────
186
+
187
+ destroy() {
188
+ if (this._rafId) {
189
+ cancelAnimationFrame(this._rafId);
190
+ this._rafId = null;
191
+ }
192
+ this.ripples = [];
193
+ super.destroy();
267
194
  }
268
195
  }
269
196