canvasframework 0.7.0 → 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.
@@ -1,146 +1,180 @@
1
1
  import Component from '../core/Component.js';
2
+
2
3
  /**
3
- * Interrupteur (toggle)
4
- * @class
5
- * @extends Component
6
- * @property {boolean} checked - État activé
7
- * @property {string} platform - Plateforme
8
- * @property {number} animProgress - Progression de l'animation
9
- * @property {boolean} isAnimating - En cours d'animation
10
- * @property {Function} onChange - Callback au changement
4
+ * Interrupteur (toggle) — Material et Cupertino.
5
+ *
6
+ * Corrections par rapport à la version précédente :
7
+ * - setInterval remplacé par requestAnimationFrame (plus précis, respecte le display rate)
8
+ * - `adjustedY` mort supprimé
9
+ * - Support `disabled` ajouté
10
+ * - destroy() annule le RAF en cours
11
11
  */
12
12
  class Switch extends Component {
13
13
  /**
14
- * Crée une instance de Switch
15
- * @param {CanvasFramework} framework - Framework parent
16
- * @param {Object} [options={}] - Options de configuration
17
- * @param {boolean} [options.checked=false] - État initial
18
- * @param {Function} [options.onChange] - Callback au changement
14
+ * @param {CanvasFramework} framework
15
+ * @param {Object} [options={}]
16
+ * @param {boolean} [options.checked=false]
17
+ * @param {boolean} [options.disabled=false]
18
+ * @param {Function} [options.onChange]
19
19
  */
20
20
  constructor(framework, options = {}) {
21
21
  super(framework, options);
22
- this.checked = options.checked || false;
22
+
23
+ this.checked = options.checked || false;
24
+ this.disabled = options.disabled || false;
23
25
  this.platform = framework.platform;
24
- this.width = 51;
26
+
27
+ // Dimensions fixes Material / Cupertino
28
+ this.width = 51;
25
29
  this.height = 31;
30
+
26
31
  this.onChange = options.onChange;
32
+
33
+ // Progression d'animation [0 = off, 1 = on]
27
34
  this.animProgress = this.checked ? 1 : 0;
28
- this.isAnimating = false;
29
-
30
- // S'assurer que le Switch est cliquable
31
- this.onClick = this.handleClick.bind(this);
35
+ this._animating = false;
36
+ this._rafId = null;
37
+
38
+ this.onClick = this._handleClick.bind(this);
32
39
  }
33
40
 
34
- /**
35
- * Gère le clic sur le switch
36
- * @private
37
- */
38
- handleClick() {
39
- console.log('Switch clicked!');
41
+ // ─────────────────────────────────────────
42
+ // INTERACTIONS
43
+ // ─────────────────────────────────────────
44
+
45
+ /** @private */
46
+ _handleClick() {
47
+ if (this.disabled) return;
48
+
40
49
  this.checked = !this.checked;
41
- if (this.onChange) this.onChange(this.checked);
42
- this.animate();
50
+ this.onChange?.(this.checked);
51
+ this._startAnimation();
43
52
  }
44
53
 
45
- /**
46
- * Anime le toggle
47
- * @private
48
- */
49
- animate() {
50
- if (this.isAnimating) return;
51
-
52
- this.isAnimating = true;
54
+ /** @private */
55
+ _startAnimation() {
56
+ if (this._animating) return;
57
+ this._animating = true;
58
+
53
59
  const target = this.checked ? 1 : 0;
54
- const step = 0.1;
55
- const interval = setInterval(() => {
56
- if (Math.abs(this.animProgress - target) < step) {
60
+
61
+ const step = () => {
62
+ if (this._destroyed) { this._rafId = null; this._animating = false; return; }
63
+
64
+ const diff = target - this.animProgress;
65
+
66
+ if (Math.abs(diff) < 0.01) {
57
67
  this.animProgress = target;
58
- clearInterval(interval);
59
- this.isAnimating = false;
60
- } else {
61
- this.animProgress += this.animProgress < target ? step : -step;
68
+ this._animating = false;
69
+ this._rafId = null;
70
+ this.markDirty();
71
+ return;
62
72
  }
63
- }, 16);
73
+
74
+ // Lerp fluide (~150 ms pour un écran 60 Hz)
75
+ this.animProgress += diff * 0.18;
76
+ this.markDirty();
77
+ this._rafId = requestAnimationFrame(step);
78
+ };
79
+
80
+ this._rafId = requestAnimationFrame(step);
64
81
  }
65
82
 
66
- /**
67
- * Dessine le switch
68
- * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
69
- */
83
+ // ─────────────────────────────────────────
84
+ // DESSIN
85
+ // ─────────────────────────────────────────
86
+
70
87
  draw(ctx) {
71
88
  ctx.save();
72
-
73
- // Déplacer le contexte avec le scroll
74
- const adjustedY = this.y;
75
-
89
+
90
+ if (this.disabled) ctx.globalAlpha = 0.38;
91
+
76
92
  if (this.platform === 'material') {
77
- // Material Design Switch
78
- const trackColor = this.checked ? 'rgba(98, 0, 238, 0.5)' : 'rgba(0, 0, 0, 0.38)';
79
- const thumbColor = this.checked ? '#6200EE' : '#FAFAFA';
80
-
81
- // Track
82
- ctx.fillStyle = trackColor;
83
- ctx.beginPath();
84
- ctx.arc(this.x + 15.5, adjustedY + 15.5, 7, Math.PI / 2, Math.PI * 1.5);
85
- ctx.arc(this.x + 35.5, adjustedY + 15.5, 7, Math.PI * 1.5, Math.PI / 2);
86
- ctx.closePath();
87
- ctx.fill();
88
-
89
- // Thumb
90
- const thumbX = this.x + 15.5 + (this.animProgress * 20);
91
- ctx.fillStyle = thumbColor;
92
- ctx.beginPath();
93
- ctx.arc(thumbX, adjustedY + 15.5, 10, 0, Math.PI * 2);
94
- ctx.fill();
95
-
96
- // Shadow for unchecked state
97
- if (!this.checked) {
98
- ctx.strokeStyle = 'rgba(0, 0, 0, 0.12)';
99
- ctx.lineWidth = 1;
100
- ctx.stroke();
101
- }
93
+ this._drawMaterial(ctx);
102
94
  } else {
103
- // Cupertino (iOS) Switch
104
- const bgColor = this.checked ? '#34C759' : '#E9E9EA';
105
-
106
- // Track
107
- ctx.fillStyle = bgColor;
108
- ctx.beginPath();
109
- ctx.arc(this.x + 15.5, adjustedY + 15.5, 15.5, Math.PI / 2, Math.PI * 1.5);
110
- ctx.arc(this.x + 35.5, adjustedY + 15.5, 15.5, Math.PI * 1.5, Math.PI / 2);
111
- ctx.closePath();
112
- ctx.fill();
113
-
114
- // Thumb with shadow
115
- const thumbX = this.x + 15.5 + (this.animProgress * 20);
116
- ctx.fillStyle = '#FFFFFF';
117
- ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
118
- ctx.shadowBlur = 4;
119
- ctx.shadowOffsetY = 2;
120
- ctx.beginPath();
121
- ctx.arc(thumbX, adjustedY + 15.5, 13.5, 0, Math.PI * 2);
122
- ctx.fill();
123
-
124
- // Reset shadow
125
- ctx.shadowColor = 'transparent';
126
- ctx.shadowBlur = 0;
127
- ctx.shadowOffsetY = 0;
95
+ this._drawCupertino(ctx);
128
96
  }
129
-
97
+
130
98
  ctx.restore();
131
99
  }
132
100
 
133
- /**
134
- * Vérifie si un point est dans les limites
135
- * @param {number} x - Coordonnée X
136
- * @param {number} y - Coordonnée Y
137
- * @returns {boolean} True si le point est dans le switch
138
- */
101
+ /** @private */
102
+ _drawMaterial(ctx) {
103
+ const trackColor = this.checked ? 'rgba(98,0,238,0.5)' : 'rgba(0,0,0,0.38)';
104
+ const thumbColor = this.checked ? '#6200EE' : '#FAFAFA';
105
+
106
+ // Track
107
+ ctx.fillStyle = trackColor;
108
+ ctx.beginPath();
109
+ ctx.arc(this.x + 15.5, this.y + 15.5, 7, Math.PI / 2, Math.PI * 1.5);
110
+ ctx.arc(this.x + 35.5, this.y + 15.5, 7, Math.PI * 1.5, Math.PI / 2);
111
+ ctx.closePath();
112
+ ctx.fill();
113
+
114
+ // Thumb
115
+ const thumbX = this.x + 15.5 + this.animProgress * 20;
116
+ ctx.fillStyle = thumbColor;
117
+ ctx.beginPath();
118
+ ctx.arc(thumbX, this.y + 15.5, 10, 0, Math.PI * 2);
119
+ ctx.fill();
120
+
121
+ if (!this.checked) {
122
+ ctx.strokeStyle = 'rgba(0,0,0,0.12)';
123
+ ctx.lineWidth = 1;
124
+ ctx.stroke();
125
+ }
126
+ }
127
+
128
+ /** @private */
129
+ _drawCupertino(ctx) {
130
+ const bgColor = this.checked ? '#34C759' : '#E9E9EA';
131
+
132
+ // Track
133
+ ctx.fillStyle = bgColor;
134
+ ctx.beginPath();
135
+ ctx.arc(this.x + 15.5, this.y + 15.5, 15.5, Math.PI / 2, Math.PI * 1.5);
136
+ ctx.arc(this.x + 35.5, this.y + 15.5, 15.5, Math.PI * 1.5, Math.PI / 2);
137
+ ctx.closePath();
138
+ ctx.fill();
139
+
140
+ // Thumb avec ombre
141
+ const thumbX = this.x + 15.5 + this.animProgress * 20;
142
+ ctx.fillStyle = '#FFFFFF';
143
+ ctx.shadowColor = 'rgba(0,0,0,0.3)';
144
+ ctx.shadowBlur = 4;
145
+ ctx.shadowOffsetY = 2;
146
+ ctx.beginPath();
147
+ ctx.arc(thumbX, this.y + 15.5, 13.5, 0, Math.PI * 2);
148
+ ctx.fill();
149
+
150
+ // Reset ombre
151
+ ctx.shadowColor = 'transparent';
152
+ ctx.shadowBlur = 0;
153
+ ctx.shadowOffsetY = 0;
154
+ }
155
+
156
+ // ─────────────────────────────────────────
157
+ // HIT TEST
158
+ // ─────────────────────────────────────────
159
+
139
160
  isPointInside(x, y) {
140
- return x >= this.x &&
141
- x <= this.x + this.width &&
142
- y >= this.y &&
143
- y <= this.y + this.height;
161
+ if (this.disabled) return false;
162
+ return (
163
+ x >= this.x && x <= this.x + this.width &&
164
+ y >= this.y && y <= this.y + this.height
165
+ );
166
+ }
167
+
168
+ // ─────────────────────────────────────────
169
+ // DESTROY
170
+ // ─────────────────────────────────────────
171
+
172
+ destroy() {
173
+ if (this._rafId) {
174
+ cancelAnimationFrame(this._rafId);
175
+ this._rafId = null;
176
+ }
177
+ super.destroy();
144
178
  }
145
179
  }
146
180
 
@@ -1,141 +1,142 @@
1
1
  import Component from '../core/Component.js';
2
+
2
3
  /**
3
- * Composant texte
4
- * @class
5
- * @extends Component
6
- * @property {string} text - Texte à afficher
7
- * @property {number} fontSize - Taille de police
8
- * @property {string} color - Couleur
9
- * @property {string} align - Alignement ('left', 'center', 'right')
10
- * @property {boolean} bold - Gras
11
- * @property {number|null} maxWidth - Largeur maximale
12
- * @property {boolean} wrap - Retour à la ligne
13
- * @property {number} lineHeight - Hauteur de ligne
14
- * @property {string[]|null} wrappedLines - Lignes après wrap
4
+ * Text — composant texte avec support du wrap et de la troncature.
5
+ *
6
+ * Corrections :
7
+ * - Calcul de x aligné pour tous les modes (center / right / left)
8
+ * - isPointInside retourne false sauf si onClick est défini
9
+ * - destroy() propre
15
10
  */
16
11
  class Text extends Component {
17
12
  /**
18
- * Crée une instance de Text
19
- * @param {CanvasFramework} framework - Framework parent
20
- * @param {Object} [options={}] - Options de configuration
21
- * @param {string} [options.text=''] - Texte
22
- * @param {number} [options.fontSize=16] - Taille de police
23
- * @param {string} [options.color='#000000'] - Couleur
24
- * @param {string} [options.align='left'] - Alignement
25
- * @param {boolean} [options.bold=false] - Gras
26
- * @param {number} [options.maxWidth] - Largeur maximale
27
- * @param {boolean} [options.wrap=false] - Retour à la ligne
28
- * @param {number} [options.lineHeight] - Hauteur de ligne
13
+ * @param {CanvasFramework} framework
14
+ * @param {Object} [options={}]
15
+ * @param {string} [options.text='']
16
+ * @param {number} [options.fontSize=16]
17
+ * @param {string} [options.color='#000000']
18
+ * @param {string} [options.align='left'] - 'left' | 'center' | 'right'
19
+ * @param {boolean} [options.bold=false]
20
+ * @param {number} [options.maxWidth] - Largeur maximale (troncature ou wrap)
21
+ * @param {boolean} [options.wrap=false] - Retour à la ligne automatique
22
+ * @param {number} [options.lineHeight] - Hauteur de ligne (défaut : fontSize × 1.2)
29
23
  */
30
24
  constructor(framework, options = {}) {
31
25
  super(framework, options);
32
- this.text = options.text || '';
33
- this.fontSize = options.fontSize || 16;
34
- this.color = options.color || '#000000';
35
- this.align = options.align || 'left';
36
- this.bold = options.bold || false;
37
- this.maxWidth = options.maxWidth || null; // Nouvelle option: largeur maximale
38
- this.wrap = options.wrap || false; // Nouvelle option: retour à la ligne
26
+
27
+ this.text = options.text || '';
28
+ this.fontSize = options.fontSize || 16;
29
+ this.color = options.color || '#000000';
30
+ this.align = options.align || 'left';
31
+ this.bold = options.bold || false;
32
+ this.maxWidth = options.maxWidth || null;
33
+ this.wrap = options.wrap || false;
39
34
  this.lineHeight = options.lineHeight || this.fontSize * 1.2;
40
-
41
- // Calculer la hauteur en fonction du texte
42
- if (this.wrap && this.maxWidth && this.text) {
43
- this.calculateWrappedHeight();
44
- }
45
- }
46
-
47
- /**
48
- * Calcule la hauteur avec wrap
49
- * @private
50
- */
51
- calculateWrappedHeight() {
52
- // Cette méthode sera appelée dans draw quand on a le contexte
53
- // Pour l'instant, on initialise juste
54
- this.wrappedLines = null;
55
35
  }
56
36
 
57
- /**
58
- * Dessine le texte
59
- * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
60
- */
37
+ // ─────────────────────────────────────────
38
+ // DESSIN
39
+ // ─────────────────────────────────────────
40
+
61
41
  draw(ctx) {
62
42
  ctx.save();
63
- ctx.fillStyle = this.color;
64
- ctx.font = `${this.bold ? 'bold ' : ''}${this.fontSize}px -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif`;
65
- ctx.textAlign = this.align;
43
+
44
+ ctx.fillStyle = this.color;
45
+ ctx.font = `${this.bold ? 'bold ' : ''}${this.fontSize}px -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif`;
46
+ ctx.textAlign = this.align;
66
47
  ctx.textBaseline = 'top';
67
-
48
+
49
+ // Calcul de l'ancre X selon l'alignement
50
+ const contentWidth = this.maxWidth || this.width;
51
+ let anchorX;
52
+ if (this.align === 'center') {
53
+ anchorX = this.x + contentWidth / 2;
54
+ } else if (this.align === 'right') {
55
+ anchorX = this.x + contentWidth;
56
+ } else {
57
+ anchorX = this.x;
58
+ }
59
+
68
60
  let lines = [this.text];
69
-
70
- // Si wrap est activé et on a une largeur max, on divise le texte
61
+
71
62
  if (this.wrap && this.maxWidth && this.text) {
72
- lines = this.wrapText(ctx, this.text, this.maxWidth);
63
+ lines = this._wrapText(ctx, this.text, this.maxWidth);
73
64
  } else if (this.maxWidth && this.text) {
74
- // Sinon, on tronque le texte avec des points de suspension
75
- const ellipsis = '...';
76
- let text = this.text;
77
- while (ctx.measureText(text).width > this.maxWidth && text.length > 3) {
78
- text = text.substring(0, text.length - 1);
79
- }
80
- if (text !== this.text && text.length > 3) {
81
- text = text.substring(0, text.length - 3) + ellipsis;
82
- }
83
- lines = [text];
65
+ lines = [this._truncateText(ctx, this.text, this.maxWidth)];
84
66
  }
85
-
86
- // Calculer la position x en fonction de l'alignement
87
- const x = this.align === 'center' ? this.x + (this.maxWidth || this.width) / 2 :
88
- this.align === 'right' ? this.x + (this.maxWidth || this.width) : this.x;
89
-
90
- // Dessiner chaque ligne
67
+
91
68
  for (let i = 0; i < lines.length; i++) {
92
- const line = lines[i];
93
- const y = this.y + (i * this.lineHeight);
94
- ctx.fillText(line, x, y);
69
+ ctx.fillText(lines[i], anchorX, this.y + i * this.lineHeight);
95
70
  }
96
-
97
- // Ajuster la hauteur si on a plusieurs lignes
71
+
72
+ // Mise à jour de la hauteur si multilignes
98
73
  if (lines.length > 1) {
99
74
  this.height = lines.length * this.lineHeight;
100
75
  }
101
-
76
+
102
77
  ctx.restore();
103
78
  }
104
79
 
105
- /**
106
- * Divise le texte en plusieurs lignes
107
- * @param {CanvasRenderingContext2D} ctx - Contexte de dessin
108
- * @param {string} text - Texte à diviser
109
- * @param {number} maxWidth - Largeur maximale
110
- * @returns {string[]} Tableau de lignes
111
- * @private
112
- */
113
- wrapText(ctx, text, maxWidth) {
114
- const words = text.split(' ');
115
- const lines = [];
116
- let currentLine = words[0];
117
-
80
+ // ─────────────────────────────────────────
81
+ // UTILITAIRES
82
+ // ─────────────────────────────────────────
83
+
84
+ /** @private */
85
+ _wrapText(ctx, text, maxWidth) {
86
+ const words = text.split(' ');
87
+ const lines = [];
88
+ let current = words[0] || '';
89
+
118
90
  for (let i = 1; i < words.length; i++) {
119
- const word = words[i];
120
- const width = ctx.measureText(currentLine + " " + word).width;
121
- if (width < maxWidth) {
122
- currentLine += " " + word;
91
+ const test = `${current} ${words[i]}`;
92
+ if (ctx.measureText(test).width <= maxWidth) {
93
+ current = test;
123
94
  } else {
124
- lines.push(currentLine);
125
- currentLine = word;
95
+ lines.push(current);
96
+ current = words[i];
126
97
  }
127
98
  }
128
- lines.push(currentLine);
99
+ lines.push(current);
129
100
  return lines;
130
101
  }
131
102
 
103
+ /** @private */
104
+ _truncateText(ctx, text, maxWidth) {
105
+ if (ctx.measureText(text).width <= maxWidth) return text;
106
+
107
+ const ellipsis = '...';
108
+ let truncated = text;
109
+
110
+ while (truncated.length > 0 && ctx.measureText(truncated + ellipsis).width > maxWidth) {
111
+ truncated = truncated.slice(0, -1);
112
+ }
113
+
114
+ return truncated + ellipsis;
115
+ }
116
+
117
+ // ─────────────────────────────────────────
118
+ // HIT TEST
119
+ // ─────────────────────────────────────────
120
+
132
121
  /**
133
- * Vérifie si un point est dans les limites
134
- * @returns {boolean} False (non cliquable)
122
+ * Retourne false par défaut (le texte n'est pas cliquable).
123
+ * Retourne true uniquement si un onClick a été défini.
135
124
  */
136
- isPointInside() {
137
- return false;
125
+ isPointInside(x, y) {
126
+ if (!this.onClick) return false;
127
+ return (
128
+ x >= this.x && x <= this.x + this.width &&
129
+ y >= this.y && y <= this.y + this.height
130
+ );
131
+ }
132
+
133
+ // ─────────────────────────────────────────
134
+ // DESTROY
135
+ // ─────────────────────────────────────────
136
+
137
+ destroy() {
138
+ super.destroy();
138
139
  }
139
140
  }
140
141
 
141
- export default Text;
142
+ export default Text;