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.
@@ -1,89 +1,102 @@
1
1
  import Component from '../core/Component.js';
2
2
 
3
3
  /**
4
- * Barre d'application supérieure (Material & Cupertino)
5
- * @class
6
- * @extends Component
4
+ * AppBar — barre d'application supérieure (Material et Cupertino).
5
+ *
6
+ * Corrections :
7
+ * - Guard _destroyed dans la boucle RAF des ripples
8
+ * - destroy() annule le RAF et nettoie les ressources
9
+ * - Fautes de frappe mineures corrigées
7
10
  */
8
11
  class AppBar extends Component {
12
+ /**
13
+ * @param {CanvasFramework} framework
14
+ * @param {Object} [options={}]
15
+ * @param {string} [options.title='']
16
+ * @param {string} [options.leftIcon] - 'menu' | 'back'
17
+ * @param {string} [options.rightIcon] - 'search' | 'more'
18
+ * @param {Function} [options.onLeftClick]
19
+ * @param {Function} [options.onRightClick]
20
+ * @param {string} [options.platform]
21
+ * @param {string} [options.bgColor]
22
+ * @param {string} [options.textColor]
23
+ * @param {number} [options.elevation]
24
+ */
9
25
  constructor(framework, options = {}) {
10
- // Détection automatique Material ou Cupertino
11
- const platform = options.platform || (() => {
12
- if (framework.platform) return framework.platform;
13
- return /iPad|iPhone|iPod/.test(navigator.userAgent) ? 'cupertino' : 'material';
14
- })();
26
+ const platform =
27
+ options.platform ||
28
+ framework.platform ||
29
+ (/iPad|iPhone|iPod/.test(navigator.userAgent) ? 'cupertino' : 'material');
15
30
 
16
31
  super(framework, {
17
32
  x: 0,
18
33
  y: 0,
19
34
  width: framework.width,
20
35
  height: options.height || (platform === 'material' ? 56 : 44),
21
- ...options
36
+ ...options,
22
37
  });
23
38
 
24
- this.title = options.title || '';
25
- this.leftIcon = options.leftIcon || null;
26
- this.rightIcon = options.rightIcon || null;
27
- this.onLeftClick = options.onLeftClick;
39
+ this.title = options.title || '';
40
+ this.leftIcon = options.leftIcon || null;
41
+ this.rightIcon = options.rightIcon || null;
42
+ this.onLeftClick = options.onLeftClick;
28
43
  this.onRightClick = options.onRightClick;
29
- this.platform = platform;
44
+ this.platform = platform;
30
45
 
31
- // Couleurs et styles par plateforme
32
46
  if (this.platform === 'material') {
33
- this.bgColor = options.bgColor || '#6200EE';
47
+ this.bgColor = options.bgColor || '#6200EE';
34
48
  this.textColor = options.textColor || '#FFFFFF';
35
49
  this.elevation = options.elevation !== undefined ? options.elevation : 4;
36
50
  } else {
37
- // Cupertino (iOS)
38
- this.bgColor = options.bgColor || 'rgba(248, 248, 248, 0.95)';
51
+ this.bgColor = options.bgColor || 'rgba(248,248,248,0.95)';
39
52
  this.textColor = options.textColor || '#000000';
40
53
  this.elevation = 0;
41
54
  }
42
55
 
43
- // Ripples (Material)
44
- this.ripples = [];
45
- this.animationFrame = null;
46
- this.lastAnimationTime = 0;
47
-
48
- // États press (iOS)
49
- this.leftPressed = false;
50
- this.rightPressed = false;
56
+ this.ripples = [];
57
+ this._rafId = null;
58
+ this._lastAnimTime = 0;
59
+ this.leftPressed = false;
60
+ this.rightPressed = false;
51
61
 
52
- this.onPress = this.handlePress.bind(this);
62
+ this.onPress = this._handlePress.bind(this);
53
63
  }
54
64
 
65
+ // ─────────────────────────────────────────
66
+ // DESSIN
67
+ // ─────────────────────────────────────────
68
+
55
69
  draw(ctx) {
56
70
  ctx.save();
57
71
 
58
72
  // Ombre Material
59
73
  if (this.platform === 'material' && this.elevation > 0) {
60
- ctx.shadowColor = 'rgba(0, 0, 0, 0.2)';
61
- ctx.shadowBlur = this.elevation * 2;
74
+ ctx.shadowColor = 'rgba(0,0,0,0.2)';
75
+ ctx.shadowBlur = this.elevation * 2;
62
76
  ctx.shadowOffsetY = this.elevation / 2;
63
77
  }
64
78
 
65
- // Background
66
79
  ctx.fillStyle = this.bgColor;
67
80
  ctx.fillRect(this.x, this.y, this.width, this.height);
68
81
 
69
- ctx.shadowColor = 'transparent';
70
- ctx.shadowBlur = 0;
82
+ ctx.shadowColor = 'transparent';
83
+ ctx.shadowBlur = 0;
71
84
  ctx.shadowOffsetY = 0;
72
85
 
73
- // Bordure iOS
86
+ // Séparateur iOS
74
87
  if (this.platform === 'cupertino') {
75
- ctx.strokeStyle = 'rgba(0, 0, 0, 0.1)';
76
- ctx.lineWidth = 0.5;
88
+ ctx.strokeStyle = 'rgba(0,0,0,0.1)';
89
+ ctx.lineWidth = 0.5;
77
90
  ctx.beginPath();
78
91
  ctx.moveTo(this.x, this.y + this.height - 0.5);
79
92
  ctx.lineTo(this.x + this.width, this.y + this.height - 0.5);
80
93
  ctx.stroke();
81
94
  }
82
95
 
83
- // Ripples pour Material
84
- if (this.platform === 'material') this.drawRipples(ctx);
96
+ // Ripples
97
+ if (this.platform === 'material') this._drawRipples(ctx);
85
98
 
86
- // Overlay pressed iOS
99
+ // Overlays pression iOS
87
100
  if (this.platform === 'cupertino') {
88
101
  if (this.leftPressed && this.leftIcon) {
89
102
  ctx.fillStyle = 'rgba(0,0,0,0.1)';
@@ -100,49 +113,52 @@ class AppBar extends Component {
100
113
  }
101
114
 
102
115
  // Titre
103
- ctx.fillStyle = this.textColor;
116
+ ctx.fillStyle = this.textColor;
104
117
  const titleAlign = this.platform === 'material' && this.leftIcon ? 'left' : 'center';
105
- const titleX = titleAlign === 'left' ? this.x + 72 : this.x + this.width / 2;
106
- ctx.font = `${this.platform === 'material' ? 'bold ' : ''}20px -apple-system, Roboto, sans-serif`;
107
- ctx.textAlign = titleAlign;
118
+ const titleX = titleAlign === 'left' ? this.x + 72 : this.x + this.width / 2;
119
+ ctx.font = `${this.platform === 'material' ? 'bold ' : ''}20px -apple-system, Roboto, sans-serif`;
120
+ ctx.textAlign = titleAlign;
108
121
  ctx.textBaseline = 'middle';
109
122
  ctx.fillText(this.title, titleX, this.y + this.height / 2);
110
123
 
111
- // Icônes
112
- if (this.leftIcon) this.drawLeftIcon(ctx);
113
- if (this.rightIcon) this.drawRightIcon(ctx);
124
+ if (this.leftIcon) this._drawLeftIcon(ctx);
125
+ if (this.rightIcon) this._drawRightIcon(ctx);
114
126
 
115
127
  ctx.restore();
116
128
  }
117
129
 
118
- drawRipples(ctx) {
119
- for (let ripple of this.ripples) {
130
+ /** @private */
131
+ _drawRipples(ctx) {
132
+ for (const r of this.ripples) {
120
133
  ctx.save();
121
- ctx.globalAlpha = ripple.opacity;
122
- ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
134
+ ctx.globalAlpha = r.opacity;
135
+ ctx.fillStyle = 'rgba(255,255,255,0.3)';
123
136
  ctx.beginPath();
124
- ctx.arc(ripple.x, ripple.y, ripple.radius, 0, Math.PI * 2);
137
+ ctx.arc(r.x, r.y, r.radius, 0, Math.PI * 2);
125
138
  ctx.fill();
126
139
  ctx.restore();
127
140
  }
128
141
  }
129
142
 
130
- drawLeftIcon(ctx) {
131
- const color = this.platform === 'cupertino' ? this.textColor : this.textColor;
132
- if (this.leftIcon === 'menu') this.drawMenuIcon(ctx, this.x + 16, this.y + this.height / 2, color);
133
- if (this.leftIcon === 'back') this.drawBackIcon(ctx, this.x + 16, this.y + this.height / 2, color);
143
+ /** @private */
144
+ _drawLeftIcon(ctx) {
145
+ const color = this.textColor;
146
+ if (this.leftIcon === 'menu') this._drawMenuIcon(ctx, this.x + 16, this.y + this.height / 2, color);
147
+ if (this.leftIcon === 'back') this._drawBackIcon(ctx, this.x + 16, this.y + this.height / 2, color);
134
148
  }
135
149
 
136
- drawRightIcon(ctx) {
150
+ /** @private */
151
+ _drawRightIcon(ctx) {
137
152
  const color = this.platform === 'cupertino' ? '#007AFF' : this.textColor;
138
- if (this.rightIcon === 'search') this.drawSearchIcon(ctx, this.x + this.width - 36, this.y + this.height / 2, color);
139
- if (this.rightIcon === 'more') this.drawMoreIcon(ctx, this.x + this.width - 36, this.y + this.height / 2, color);
153
+ if (this.rightIcon === 'search') this._drawSearchIcon(ctx, this.x + this.width - 36, this.y + this.height / 2, color);
154
+ if (this.rightIcon === 'more') this._drawMoreIcon(ctx, this.x + this.width - 36, this.y + this.height / 2, color);
140
155
  }
141
156
 
142
- drawMenuIcon(ctx, x, y, color) {
157
+ /** @private */
158
+ _drawMenuIcon(ctx, x, y, color) {
143
159
  ctx.strokeStyle = color;
144
- ctx.lineWidth = 2;
145
- ctx.lineCap = 'round';
160
+ ctx.lineWidth = 2;
161
+ ctx.lineCap = 'round';
146
162
  for (let i = 0; i < 3; i++) {
147
163
  ctx.beginPath();
148
164
  ctx.moveTo(x, y - 8 + i * 8);
@@ -151,22 +167,24 @@ class AppBar extends Component {
151
167
  }
152
168
  }
153
169
 
154
- drawBackIcon(ctx, x, y, color) {
170
+ /** @private */
171
+ _drawBackIcon(ctx, x, y, color) {
155
172
  ctx.strokeStyle = color;
156
- ctx.lineWidth = 2;
157
- ctx.lineCap = 'round';
158
- ctx.lineJoin = 'round';
173
+ ctx.lineWidth = 2;
174
+ ctx.lineCap = 'round';
175
+ ctx.lineJoin = 'round';
159
176
  ctx.beginPath();
160
177
  ctx.moveTo(x + 16, y - 10);
161
- ctx.lineTo(x + 6, y);
178
+ ctx.lineTo(x + 6, y);
162
179
  ctx.lineTo(x + 16, y + 10);
163
180
  ctx.stroke();
164
181
  }
165
182
 
166
- drawSearchIcon(ctx, x, y, color) {
183
+ /** @private */
184
+ _drawSearchIcon(ctx, x, y, color) {
167
185
  ctx.strokeStyle = color;
168
- ctx.lineWidth = 2;
169
- ctx.lineCap = 'round';
186
+ ctx.lineWidth = 2;
187
+ ctx.lineCap = 'round';
170
188
  ctx.beginPath();
171
189
  ctx.arc(x + 8, y - 2, 8, 0, Math.PI * 2);
172
190
  ctx.stroke();
@@ -176,9 +194,10 @@ class AppBar extends Component {
176
194
  ctx.stroke();
177
195
  }
178
196
 
179
- drawMoreIcon(ctx, x, y, color) {
180
- ctx.fillStyle = color;
181
- const spacing = this.platform === 'material' ? 10 : 8;
197
+ /** @private */
198
+ _drawMoreIcon(ctx, x, y, color) {
199
+ ctx.fillStyle = color;
200
+ const spacing = this.platform === 'material' ? 10 : 8;
182
201
  for (let i = 0; i < 3; i++) {
183
202
  ctx.beginPath();
184
203
  ctx.arc(x + 12, y - spacing + i * spacing, 2, 0, Math.PI * 2);
@@ -186,71 +205,112 @@ class AppBar extends Component {
186
205
  }
187
206
  }
188
207
 
189
- handlePress(x, y) {
190
- const inY = y >= this.y && y <= this.y + this.height;
208
+ // ─────────────────────────────────────────
209
+ // INTERACTION
210
+ // ─────────────────────────────────────────
191
211
 
212
+ /** @private */
213
+ _handlePress(x, y) {
214
+ const inY = y >= this.y && y <= this.y + this.height;
192
215
  if (!inY) return false;
193
216
 
194
217
  // Bouton gauche
195
218
  if (this.leftIcon && x >= this.x && x <= this.x + 56) {
196
- if (this.platform === 'material') this.addRipple(this.x + 28, this.y + this.height / 2);
197
- else this.leftPressed = true, setTimeout(() => this.leftPressed = false, 150);
198
- if (this.onLeftClick) this.onLeftClick();
219
+ if (this.platform === 'material') {
220
+ this._addRipple(this.x + 28, this.y + this.height / 2);
221
+ } else {
222
+ this.leftPressed = true;
223
+ setTimeout(() => { this.leftPressed = false; this.markDirty(); }, 150);
224
+ }
225
+ this.onLeftClick?.();
199
226
  return true;
200
227
  }
201
228
 
202
229
  // Bouton droit
203
230
  if (this.rightIcon && x >= this.x + this.width - 56 && x <= this.x + this.width) {
204
- if (this.platform === 'material') this.addRipple(this.x + this.width - 28, this.y + this.height / 2);
205
- else this.rightPressed = true, setTimeout(() => this.rightPressed = false, 150);
206
- if (this.onRightClick) this.onRightClick();
231
+ if (this.platform === 'material') {
232
+ this._addRipple(this.x + this.width - 28, this.y + this.height / 2);
233
+ } else {
234
+ this.rightPressed = true;
235
+ setTimeout(() => { this.rightPressed = false; this.markDirty(); }, 150);
236
+ }
237
+ this.onRightClick?.();
207
238
  return true;
208
239
  }
209
240
 
210
241
  return false;
211
242
  }
212
243
 
213
- addRipple(x, y) {
244
+ // ─────────────────────────────────────────
245
+ // RIPPLE
246
+ // ─────────────────────────────────────────
247
+
248
+ /** @private */
249
+ _addRipple(x, y) {
214
250
  this.ripples.push({
215
251
  x, y,
216
252
  radius: 0,
217
253
  maxRadius: 28,
218
254
  opacity: 1,
219
- createdAt: performance.now()
255
+ createdAt: performance.now(),
220
256
  });
221
- if (!this.animationFrame) this.startRippleAnimation();
257
+ if (!this._rafId) this._startRippleAnimation();
222
258
  }
223
259
 
224
- startRippleAnimation() {
260
+ /** @private */
261
+ _startRippleAnimation() {
225
262
  const animate = (timestamp) => {
226
- if (!this.lastAnimationTime) this.lastAnimationTime = timestamp;
227
- const deltaTime = timestamp - this.lastAnimationTime;
228
- this.lastAnimationTime = timestamp;
263
+ if (this._destroyed) { this._rafId = null; this._lastAnimTime = 0; return; }
264
+
265
+ const deltaTime = this._lastAnimTime ? timestamp - this._lastAnimTime : 16;
266
+ this._lastAnimTime = timestamp;
267
+
229
268
  let needsUpdate = false;
230
269
 
231
270
  for (let i = this.ripples.length - 1; i >= 0; i--) {
232
- const ripple = this.ripples[i];
233
- if (ripple.radius < ripple.maxRadius) ripple.radius += (ripple.maxRadius / 250) * deltaTime, needsUpdate = true;
234
- if (ripple.radius >= ripple.maxRadius * 0.4) ripple.opacity -= (0.003 * deltaTime), ripple.opacity < 0 && (ripple.opacity = 0), needsUpdate = true;
235
- if (ripple.opacity <= 0 && ripple.radius >= ripple.maxRadius * 0.95) this.ripples.splice(i, 1), needsUpdate = true;
271
+ const r = this.ripples[i];
272
+ if (r.radius < r.maxRadius) {
273
+ r.radius += (r.maxRadius / 250) * deltaTime;
274
+ needsUpdate = true;
275
+ }
276
+ if (r.radius >= r.maxRadius * 0.4) {
277
+ r.opacity -= 0.003 * deltaTime;
278
+ if (r.opacity < 0) r.opacity = 0;
279
+ needsUpdate = true;
280
+ }
281
+ if (r.opacity <= 0 && r.radius >= r.maxRadius * 0.95) {
282
+ this.ripples.splice(i, 1);
283
+ needsUpdate = true;
284
+ }
236
285
  }
237
286
 
238
- if (needsUpdate) this.requestRender();
239
- if (this.ripples.length > 0) this.animationFrame = requestAnimationFrame(animate);
240
- else this.animationFrame = null, this.lastAnimationTime = 0;
287
+ if (needsUpdate) this.markDirty();
288
+
289
+ if (this.ripples.length > 0) {
290
+ this._rafId = requestAnimationFrame(animate);
291
+ } else {
292
+ this._rafId = null;
293
+ this._lastAnimTime = 0;
294
+ }
241
295
  };
242
296
 
243
- if (this.ripples.length > 0 && !this.animationFrame) this.animationFrame = requestAnimationFrame(animate);
297
+ if (this.ripples.length > 0 && !this._rafId) {
298
+ this._rafId = requestAnimationFrame(animate);
299
+ }
244
300
  }
245
301
 
246
- requestRender() {
247
- if (this.framework && this.framework.requestRender) this.framework.requestRender();
248
- }
302
+ // ─────────────────────────────────────────
303
+ // DESTROY
304
+ // ─────────────────────────────────────────
249
305
 
250
306
  destroy() {
251
- if (this.animationFrame) cancelAnimationFrame(this.animationFrame), this.animationFrame = null;
307
+ if (this._rafId) {
308
+ cancelAnimationFrame(this._rafId);
309
+ this._rafId = null;
310
+ }
311
+ this.ripples = [];
252
312
  super.destroy();
253
313
  }
254
314
  }
255
315
 
256
- export default AppBar;
316
+ export default AppBar;