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,166 +1,173 @@
1
1
  import Component from '../core/Component.js';
2
+ import { roundRect } from '../core/CanvasUtils.js';
2
3
 
3
4
  /**
4
- * Checkbox Material & Cupertino (iOS-like)
5
+ * Checkbox Material et Cupertino.
6
+ *
7
+ * Corrections :
8
+ * - roundRect() vient de CanvasUtils
9
+ * - Support `disabled` ajouté
10
+ * - destroy() propre
5
11
  */
6
12
  class Checkbox extends Component {
13
+ /**
14
+ * @param {CanvasFramework} framework
15
+ * @param {Object} [options={}]
16
+ * @param {boolean} [options.checked=false]
17
+ * @param {boolean} [options.disabled=false]
18
+ * @param {string} [options.label='']
19
+ * @param {Function} [options.onChange]
20
+ */
7
21
  constructor(framework, options = {}) {
8
22
  super(framework, options);
9
23
 
10
- this.checked = !!options.checked;
11
- this.label = options.label || '';
24
+ this.checked = !!options.checked;
25
+ this.disabled = !!options.disabled;
26
+ this.label = options.label || '';
12
27
  this.platform = framework.platform;
13
28
  this.onChange = options.onChange;
14
29
 
15
30
  this.boxSize = 22;
16
31
  this.padding = 10;
17
32
 
18
- this.textWidth = this.label
19
- ? this.getTextWidth(this.label)
20
- : 0;
33
+ this.textWidth = this.label ? this._getTextWidth(this.label) : 0;
21
34
 
22
- // Largeur totale
23
35
  this.width =
24
36
  this.platform === 'material'
25
37
  ? this.boxSize + this.padding + this.textWidth
26
- : this.textWidth + 28; // place pour checkmark iOS
38
+ : this.textWidth + 28;
27
39
 
28
40
  this.height = 28;
29
41
 
30
42
  this.onClick = () => {
43
+ if (this.disabled) return;
31
44
  this.checked = !this.checked;
32
45
  this.onChange?.(this.checked);
46
+ this.markDirty();
33
47
  };
34
48
  }
35
49
 
36
- getTextWidth(text) {
50
+ /** @private */
51
+ _getTextWidth(text) {
37
52
  const ctx = this.framework.ctx;
38
53
  ctx.save();
39
- ctx.font = '16px -apple-system, system-ui, sans-serif';
40
- const w = ctx.measureText(text).width;
54
+ ctx.font = '16px -apple-system, system-ui, sans-serif';
55
+ const w = ctx.measureText(text).width;
41
56
  ctx.restore();
42
57
  return w;
43
58
  }
44
59
 
60
+ // ─────────────────────────────────────────
61
+ // DESSIN
62
+ // ─────────────────────────────────────────
63
+
45
64
  draw(ctx) {
46
65
  ctx.save();
47
- ctx.font = '16px -apple-system, system-ui, sans-serif';
66
+
67
+ if (this.disabled) ctx.globalAlpha = 0.38;
68
+
69
+ ctx.font = '16px -apple-system, system-ui, sans-serif';
48
70
  ctx.textBaseline = 'middle';
49
71
 
50
72
  const centerY = this.y + this.height / 2;
51
73
 
52
74
  if (this.platform === 'material') {
53
- this.drawMaterial(ctx, centerY);
75
+ this._drawMaterial(ctx, centerY);
54
76
  } else {
55
- this.drawCupertino(ctx, centerY);
77
+ this._drawCupertino(ctx, centerY);
56
78
  }
57
79
 
58
80
  ctx.restore();
59
81
  }
60
82
 
61
- /* ---------------- MATERIAL ---------------- */
62
-
63
- drawMaterial(ctx, centerY) {
83
+ /** @private */
84
+ _drawMaterial(ctx, centerY) {
64
85
  const x = this.x;
65
86
  const y = centerY - this.boxSize / 2;
66
87
 
67
- // Box
68
- ctx.lineWidth = 2;
88
+ ctx.lineWidth = 2;
69
89
  ctx.strokeStyle = this.checked ? '#6200EE' : '#757575';
70
- ctx.fillStyle = this.checked ? '#6200EE' : 'transparent';
90
+ ctx.fillStyle = this.checked ? '#6200EE' : 'transparent';
71
91
 
72
- this.roundRect(ctx, x, y, this.boxSize, this.boxSize, 3);
92
+ ctx.beginPath();
93
+ roundRect(ctx, x, y, this.boxSize, this.boxSize, 3);
73
94
  if (this.checked) ctx.fill();
74
95
  ctx.stroke();
75
96
 
76
- // Check
77
97
  if (this.checked) {
78
98
  ctx.strokeStyle = '#FFF';
79
- ctx.lineWidth = 2.4;
99
+ ctx.lineWidth = 2.4;
100
+ ctx.lineCap = 'round';
101
+ ctx.lineJoin = 'round';
80
102
  ctx.beginPath();
81
- ctx.moveTo(x + 5, y + 12);
82
- ctx.lineTo(x + 9, y + 16);
103
+ ctx.moveTo(x + 5, y + 12);
104
+ ctx.lineTo(x + 9, y + 16);
83
105
  ctx.lineTo(x + 17, y + 7);
84
106
  ctx.stroke();
85
107
  }
86
108
 
87
- // Label
88
- ctx.fillStyle = '#000';
89
- ctx.fillText(
90
- this.label,
91
- x + this.boxSize + this.padding,
92
- centerY
93
- );
109
+ if (this.label) {
110
+ ctx.fillStyle = '#000';
111
+ ctx.fillText(this.label, x + this.boxSize + this.padding, centerY);
112
+ }
94
113
  }
95
114
 
96
- /* ---------------- CUPERTINO ---------------- */
97
-
98
- /* ---------------- CUPERTINO ---------------- */
99
-
100
- drawCupertino(ctx, centerY) {
101
- const radius = 10;
102
- const circleX = this.x + radius;
103
- const circleY = centerY;
115
+ /** @private */
116
+ _drawCupertino(ctx, centerY) {
117
+ const radius = 10;
118
+ const circleX = this.x + radius;
119
+ const circleY = centerY;
104
120
 
105
- // Cercle
106
- if (this.checked) {
107
- ctx.fillStyle = '#007AFF'; // Apple blue
108
- ctx.beginPath();
109
- ctx.arc(circleX, circleY, radius, 0, Math.PI * 2);
110
- ctx.fill();
111
- } else {
112
- ctx.strokeStyle = '#C7C7CC'; // iOS gray
113
- ctx.lineWidth = 2;
114
- ctx.beginPath();
115
- ctx.arc(circleX, circleY, radius, 0, Math.PI * 2);
116
- ctx.stroke();
117
- }
121
+ if (this.checked) {
122
+ ctx.fillStyle = '#007AFF';
123
+ ctx.beginPath();
124
+ ctx.arc(circleX, circleY, radius, 0, Math.PI * 2);
125
+ ctx.fill();
126
+ } else {
127
+ ctx.strokeStyle = '#C7C7CC';
128
+ ctx.lineWidth = 2;
129
+ ctx.beginPath();
130
+ ctx.arc(circleX, circleY, radius, 0, Math.PI * 2);
131
+ ctx.stroke();
132
+ }
118
133
 
119
- // Checkmark
120
- if (this.checked) {
121
- ctx.strokeStyle = '#FFFFFF';
122
- ctx.lineWidth = 2.2;
123
- ctx.lineCap = 'round';
124
- ctx.lineJoin = 'round';
134
+ if (this.checked) {
135
+ ctx.strokeStyle = '#FFFFFF';
136
+ ctx.lineWidth = 2.2;
137
+ ctx.lineCap = 'round';
138
+ ctx.lineJoin = 'round';
139
+ ctx.beginPath();
140
+ ctx.moveTo(circleX - 4, circleY);
141
+ ctx.lineTo(circleX - 1, circleY + 3);
142
+ ctx.lineTo(circleX + 5, circleY - 4);
143
+ ctx.stroke();
144
+ }
125
145
 
126
- ctx.beginPath();
127
- ctx.moveTo(circleX - 4, circleY);
128
- ctx.lineTo(circleX - 1, circleY + 3);
129
- ctx.lineTo(circleX + 5, circleY - 4);
130
- ctx.stroke();
146
+ if (this.label) {
147
+ ctx.fillStyle = '#000';
148
+ ctx.fillText(this.label, this.x + radius * 2 + this.padding, centerY);
149
+ }
131
150
  }
132
151
 
133
- // Label
134
- ctx.fillStyle = '#000';
135
- ctx.fillText(
136
- this.label,
137
- this.x + radius * 2 + this.padding,
138
- centerY
139
- );
140
- }
141
-
142
- roundRect(ctx, x, y, w, h, r) {
143
- ctx.beginPath();
144
- ctx.moveTo(x + r, y);
145
- ctx.lineTo(x + w - r, y);
146
- ctx.quadraticCurveTo(x + w, y, x + w, y + r);
147
- ctx.lineTo(x + w, y + h - r);
148
- ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
149
- ctx.lineTo(x + r, y + h);
150
- ctx.quadraticCurveTo(x, y + h, x, y + h - r);
151
- ctx.lineTo(x, y + r);
152
- ctx.quadraticCurveTo(x, y, x + r, y);
153
- ctx.closePath();
154
- }
152
+ // ─────────────────────────────────────────
153
+ // HIT TEST
154
+ // ─────────────────────────────────────────
155
155
 
156
156
  isPointInside(x, y) {
157
+ if (this.disabled) return false;
157
158
  return (
158
- x >= this.x &&
159
- x <= this.x + this.width &&
160
- y >= this.y &&
161
- y <= this.y + this.height
159
+ x >= this.x && x <= this.x + this.width &&
160
+ y >= this.y && y <= this.y + this.height
162
161
  );
163
162
  }
163
+
164
+ // ─────────────────────────────────────────
165
+ // DESTROY
166
+ // ─────────────────────────────────────────
167
+
168
+ destroy() {
169
+ super.destroy();
170
+ }
164
171
  }
165
172
 
166
- export default Checkbox;
173
+ export default Checkbox;
@@ -1,118 +1,183 @@
1
1
  import Component from '../core/Component.js';
2
-
2
+ import { roundRect, hexToRgb, darkenColor } from '../core/CanvasUtils.js';
3
+
4
+ /**
5
+ * Chip — Material et Cupertino.
6
+ *
7
+ * Corrections :
8
+ * - roundRect / darkenColor / hexToRgb viennent de CanvasUtils
9
+ * - Guard _destroyed dans le RAF de ripple
10
+ * - destroy() annule le RAF
11
+ */
3
12
  class Chip extends Component {
13
+ /**
14
+ * @param {CanvasFramework} framework
15
+ * @param {Object} [options={}]
16
+ * @param {string} [options.text='']
17
+ * @param {string} [options.icon] - Emoji ou caractère Unicode
18
+ * @param {boolean} [options.closable=true]
19
+ * @param {string} [options.bgColor]
20
+ * @param {string} [options.textColor]
21
+ * @param {string} [options.rippleColor]
22
+ * @param {Function} [options.onClose]
23
+ */
4
24
  constructor(framework, options = {}) {
5
25
  super(framework, options);
6
26
 
7
- this.text = options.text || '';
8
- this.icon = options.icon || null;
27
+ this.text = options.text || '';
28
+ this.icon = options.icon || null;
9
29
  this.closable = options.closable !== false;
10
- this.platform = framework.platform; // 'material' ou 'cupertino'
11
- this.onClose = options.onClose;
30
+ this.platform = framework.platform;
31
+ this.onClose = options.onClose;
12
32
 
13
- // Couleurs par défaut selon platform
14
33
  if (this.platform === 'material') {
15
- this.bgColor = options.bgColor || '#E0E0E0';
16
- this.textColor = options.textColor || '#1F1F1F';
34
+ this.bgColor = options.bgColor || '#E0E0E0';
35
+ this.textColor = options.textColor || '#1F1F1F';
17
36
  this.rippleColor = options.rippleColor || 'rgba(0,0,0,0.12)';
18
- } else { // Cupertino
19
- this.bgColor = options.bgColor || 'rgba(242,242,247,0.95)';
37
+ } else {
38
+ this.bgColor = options.bgColor || 'rgba(242,242,247,0.95)';
20
39
  this.textColor = options.textColor || '#000';
21
40
  }
22
41
 
23
- // Dimensions
24
- const ctx = framework.ctx;
25
- ctx.font = '14px -apple-system, sans-serif';
42
+ const ctx = framework.ctx;
43
+ ctx.font = '14px -apple-system, sans-serif';
26
44
  const textWidth = ctx.measureText(this.text).width;
27
- const iconWidth = this.icon ? 20 : 0;
28
- const closeWidth = this.closable ? 24 : 0;
29
- this.width = iconWidth + textWidth + closeWidth + 24;
30
- this.height = options.height || 32;
31
- this.borderRadius = this.height / 2;
45
+ const iconWidth = this.icon ? 20 : 0;
46
+ const closeWide = this.closable ? 24 : 0;
32
47
 
33
- // Ripple pour Material
34
- this.ripples = [];
35
- this.pressed = false;
48
+ this.width = iconWidth + textWidth + closeWide + 24;
49
+ this.height = options.height || 32;
50
+ this.borderRadius = this.height / 2;
36
51
 
52
+ this.ripples = [];
53
+ this._rafId = null;
54
+ this.pressed = false;
37
55
  this.closeButtonRect = null;
38
- this.onPress = this.handlePress.bind(this);
56
+
57
+ this.onPress = this._handlePress.bind(this);
39
58
  }
40
59
 
41
- addRipple(x, y) {
42
- const ripple = {
60
+ // ─────────────────────────────────────────
61
+ // RIPPLE
62
+ // ─────────────────────────────────────────
63
+
64
+ /** @private */
65
+ _addRipple(x, y) {
66
+ this.ripples.push({
43
67
  x, y,
44
68
  radius: 0,
45
69
  maxRadius: Math.max(this.width, this.height) * 1.5,
46
- opacity: 0.3
47
- };
48
- this.ripples.push(ripple);
70
+ opacity: 0.3,
71
+ });
72
+ this._animateRipple();
73
+ }
74
+
75
+ /** @private */
76
+ _animateRipple() {
77
+ if (this._rafId) return;
49
78
 
50
79
  const animate = () => {
80
+ if (this._destroyed) { this._rafId = null; return; }
81
+
51
82
  let active = false;
52
- for (let r of this.ripples) {
53
- if (r.radius < r.maxRadius) {
54
- r.radius += r.maxRadius / 15;
55
- r.opacity -= 0.03;
56
- active = true;
57
- }
83
+ for (const r of this.ripples) {
84
+ if (r.radius < r.maxRadius) { r.radius += r.maxRadius / 15; active = true; }
85
+ r.opacity -= 0.03;
86
+ }
87
+ this.ripples = this.ripples.filter((r) => r.opacity > 0);
88
+ this.markDirty();
89
+
90
+ if (active && this.ripples.length > 0) {
91
+ this._rafId = requestAnimationFrame(animate);
92
+ } else {
93
+ this._rafId = null;
58
94
  }
59
- this.ripples = this.ripples.filter(r => r.opacity > 0);
60
- if (active) requestAnimationFrame(animate);
61
95
  };
62
- animate();
96
+
97
+ this._rafId = requestAnimationFrame(animate);
63
98
  }
64
99
 
100
+ // ─────────────────────────────────────────
101
+ // INTERACTION
102
+ // ─────────────────────────────────────────
103
+
104
+ /** @private */
105
+ _handlePress(x, y) {
106
+ // Bouton fermeture
107
+ if (this.closable && this.closeButtonRect) {
108
+ const r = this.closeButtonRect;
109
+ if (x >= r.x && x <= r.x + r.width && y >= r.y && y <= r.y + r.height) {
110
+ this.onClose?.();
111
+ return;
112
+ }
113
+ }
114
+
115
+ if (this.platform === 'material') {
116
+ this._addRipple(x - this.x, y - this.y);
117
+ } else {
118
+ this.pressed = true;
119
+ setTimeout(() => { this.pressed = false; this.markDirty(); }, 100);
120
+ }
121
+
122
+ this.onClick?.();
123
+ }
124
+
125
+ // ─────────────────────────────────────────
126
+ // DESSIN
127
+ // ─────────────────────────────────────────
128
+
65
129
  draw(ctx) {
66
130
  ctx.save();
67
131
 
68
- // Background
132
+ // Fond
69
133
  ctx.fillStyle = this.pressed && this.platform === 'cupertino'
70
- ? this.darkenColor(this.bgColor, 0.1)
134
+ ? darkenColor(this.bgColor)
71
135
  : this.bgColor;
72
136
 
73
137
  ctx.beginPath();
74
- this.roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
138
+ roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
75
139
  ctx.fill();
76
140
 
77
141
  let offsetX = this.x + 12;
78
142
 
79
- // Icon
143
+ // Icône
80
144
  if (this.icon) {
81
- ctx.font = '16px sans-serif';
82
- ctx.fillStyle = this.textColor;
83
- ctx.textAlign = 'left';
145
+ ctx.font = '16px sans-serif';
146
+ ctx.fillStyle = this.textColor;
147
+ ctx.textAlign = 'left';
84
148
  ctx.textBaseline = 'middle';
85
149
  ctx.fillText(this.icon, offsetX, this.y + this.height / 2);
86
150
  offsetX += 20;
87
151
  }
88
152
 
89
- // Text
90
- ctx.font = this.platform === 'material'
153
+ // Texte
154
+ ctx.font = this.platform === 'material'
91
155
  ? '500 14px Roboto, sans-serif'
92
156
  : '400 14px -apple-system';
93
- ctx.fillStyle = this.textColor;
157
+ ctx.fillStyle = this.textColor;
158
+ ctx.textAlign = 'left';
159
+ ctx.textBaseline = 'middle';
94
160
  ctx.fillText(this.text, offsetX, this.y + this.height / 2);
95
161
 
96
162
  // Ripple (Material)
97
- if (this.platform === 'material') {
163
+ if (this.platform === 'material' && this.ripples.length > 0) {
98
164
  ctx.save();
99
165
  ctx.beginPath();
100
- this.roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
166
+ roundRect(ctx, this.x, this.y, this.width, this.height, this.borderRadius);
101
167
  ctx.clip();
102
168
 
103
- this.ripples.forEach(r => {
169
+ for (const r of this.ripples) {
104
170
  ctx.globalAlpha = r.opacity;
105
- ctx.fillStyle = this.rippleColor;
171
+ ctx.fillStyle = this.rippleColor;
106
172
  ctx.beginPath();
107
173
  ctx.arc(this.x + r.x, this.y + r.y, r.radius, 0, Math.PI * 2);
108
174
  ctx.fill();
109
- });
110
-
175
+ }
111
176
  ctx.restore();
112
177
  ctx.globalAlpha = 1;
113
178
  }
114
179
 
115
- // Close button
180
+ // Bouton fermeture
116
181
  if (this.closable) {
117
182
  const cx = this.x + this.width - 20;
118
183
  const cy = this.y + this.height / 2;
@@ -124,70 +189,36 @@ class Chip extends Component {
124
189
  ctx.fill();
125
190
 
126
191
  ctx.strokeStyle = this.textColor;
127
- ctx.lineWidth = 1.5;
128
- ctx.lineCap = 'round';
192
+ ctx.lineWidth = 1.5;
193
+ ctx.lineCap = 'round';
129
194
  ctx.beginPath();
130
- ctx.moveTo(cx - 4, cy - 4);
131
- ctx.lineTo(cx + 4, cy + 4);
132
- ctx.moveTo(cx + 4, cy - 4);
133
- ctx.lineTo(cx - 4, cy + 4);
195
+ ctx.moveTo(cx - 4, cy - 4); ctx.lineTo(cx + 4, cy + 4);
196
+ ctx.moveTo(cx + 4, cy - 4); ctx.lineTo(cx - 4, cy + 4);
134
197
  ctx.stroke();
135
198
  }
136
199
 
137
200
  ctx.restore();
138
201
  }
139
202
 
140
- handlePress(x, y) {
141
- // Vérifie bouton close
142
- if (this.closable && this.closeButtonRect) {
143
- if (x >= this.closeButtonRect.x && x <= this.closeButtonRect.x + this.closeButtonRect.width &&
144
- y >= this.closeButtonRect.y && y <= this.closeButtonRect.y + this.closeButtonRect.height) {
145
- this.onClose?.();
146
- return;
147
- }
148
- }
149
-
150
- // Ripple Material
151
- if (this.platform === 'material') {
152
- this.addRipple(x - this.x, y - this.y);
153
- } else {
154
- // Feedback press Cupertino
155
- this.pressed = true;
156
- setTimeout(() => { this.pressed = false; this.markDirty(); }, 100);
157
- }
158
-
159
- this.onClick?.();
203
+ isPointInside(x, y) {
204
+ return (
205
+ x >= this.x && x <= this.x + this.width &&
206
+ y >= this.y && y <= this.y + this.height
207
+ );
160
208
  }
161
209
 
162
- roundRect(ctx, x, y, w, h, r) {
163
- ctx.moveTo(x + r, y);
164
- ctx.lineTo(x + w - r, y);
165
- ctx.quadraticCurveTo(x + w, y, x + w, y + r);
166
- ctx.lineTo(x + w, y + h - r);
167
- ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
168
- ctx.lineTo(x + r, y + h);
169
- ctx.quadraticCurveTo(x, y + h, x, y + h - r);
170
- ctx.lineTo(x, y + r);
171
- ctx.quadraticCurveTo(x, y, x + r, y);
172
- ctx.closePath();
173
- }
210
+ // ─────────────────────────────────────────
211
+ // DESTROY
212
+ // ─────────────────────────────────────────
174
213
 
175
- darkenColor(color, factor = 0.2) {
176
- if (color.startsWith('#')) {
177
- const { r, g, b } = this.hexToRgb(color);
178
- return `rgb(${Math.floor(r * (1 - factor))}, ${Math.floor(g * (1 - factor))}, ${Math.floor(b * (1 - factor))})`;
214
+ destroy() {
215
+ if (this._rafId) {
216
+ cancelAnimationFrame(this._rafId);
217
+ this._rafId = null;
179
218
  }
180
- return color;
181
- }
182
-
183
- hexToRgb(hex) {
184
- const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
185
- return result ? {
186
- r: parseInt(result[1], 16),
187
- g: parseInt(result[2], 16),
188
- b: parseInt(result[3], 16)
189
- } : { r: 0, g: 0, b: 0 };
219
+ this.ripples = [];
220
+ super.destroy();
190
221
  }
191
222
  }
192
223
 
193
- export default Chip;
224
+ export default Chip;